翻译 - BlackHat USA 2010 - Understanding the Low Fragmentation Heap

1、这篇文章是对BlackHat USA 2010上Chris Valasek的议题《Understanding the Low Fragmentation Heap》的翻译。
2、在我分析CVE-2012-1876的过程中,对漏洞利用部分的堆布局不是很懂,所以找到了这篇文章进行翻译,以更好地理解漏洞利用中的堆布局。
3、LFH(Low Fragmentation Heap)是在Windows Vista版本中引入的。
4、本文是基于32位Windows 7 RTM版本的ntdll.dll(6.1.7600.16385)来进行分析LFH的结构的。
                            ——当你的才华还配不上你的野心时,请静下来好好努力!

Introduction(简介)

  多年来,由于增加了漏洞利用对抗措施以及实施了更复杂的算法和数据结构Windows堆利用的难度持续增加。由于这些趋势以及社区中缺乏全面的堆相关知识,使得可靠的漏洞利用已严重下降。保持对堆管理器内部工作原理的全面的理解,可以区分不可预测的错误精确的漏洞利用

  自Windows Vista的引入,低碎片堆(Low Fragmentation heap)已成为Windows操作系统的默认前端堆管理器(Front-End)。这个新的前端堆管理器引入了一组不同的数据结构和算法,这些数据结构和算法取代了快表(Lookaside)。同时,该系统还改变了后端堆管理器的工作方式。必须仔细阅读所有这些资料,以理解这些变化对在Windows 7上的应用程序中分配和释放内存所产生的影响。

  本文的主要目的是使读者熟悉与低碎片堆相关的新创建的逻辑数据结构。首先,通过解释堆管理器中的新数据结构及其耦合关系,将提供一个清晰简洁的基础。然后,将讨论有关操纵这些数据结构的底层算法的详细说明。最后,将揭秘一些新的漏洞利用开发技术,同时提供一些使用这些新发现的技术的实际应用范例

Overview(概述)

  本文分为 四个单独的部分第一部分 详细介绍了核心数据结构,这些数据结构贯穿于整个堆管理器中,被用于维护内存。对这些数据结构有一个透彻的理解是理解本文中其余部分内容前提

  第二部分 将讨论Windows Vista中引入并在Windows 7中继续使用的新创建的架构(Architecture)。此部分会展示从Windows XP代码库演变而来的数据结构是如何使用的。

  第三部分 将深入探讨Windows 7堆使用的核心算法。本节将对前端后端堆管理子系统进行详细介绍。理解本节中的内容将有助于漏洞利用开发,并为第四节提供框架

  第四部分 ,也是本文的最后一部分,将展示如何使用底层堆管理器,对堆进行精心操纵,产生可靠的堆操作的策略,通过用户提供的信息,并滥用堆元数据,以实现代码执行

Prior Works(先前的工作)

  尽管当前可能有非常多的有关“低碎片堆(Low Fragmentation heap)”的信息,但我还是仅列出我进行研究时使用的一些资料。我认为这些资料应被视为理解本文的必读资料。对于我可能在此列表遗漏的任何人,我预先在这里致歉

  • 我仍然坚信本·霍克斯(Ben Hawkes)在几年前就知道了这一点。他于2008年RuxCon/Blackhat上的演讲仍然是我工作的灵感。(Hawkes 2008年)
  • Nico WaismanWindows Vista做了大量的逆向工作,并在Immunity Debuggerlibheap.py插件中提供了详细的信息。(Waisman 2008)
  • 我认为,布雷特·摩尔(Brett Moore)的论文《Heaps about Heaps》是有史以来最好的堆演示文稿之一。我认为它将永远用作大量堆相关工作参考资料。(Moore 2007)
  • 布雷特·摩尔(Brett Moore)还发布了利用Windows XP SP2FreeList[0]Link过程的利用手法,本文也会提及此手法。(Moore 2005)
  • 理查德·约翰逊(Richard Johnson)ToorCon 2006上的演讲描述了为Windows Vista新创建的“低碎片堆(Low Fragmentation Heap)”。这是第一个(也许是唯一一个)揭示有关LFH算法和数据结构的详细信息的资料。(Johnson 2006)
  • 尽管David B. Probert(Ph.D.)的演讲主要是针对“低碎片堆(Low Fragmentation Heap)”性能优势,但对于试图理解在Windows 7中的堆实现的变化背后的原因时,它仍然非常有价值。(Probert)
  • Adrian MarinescuBlackhat 2006上就Windows Vista堆实现变化进行了介绍。它清楚地显示了从旧堆管理机制过渡到当下的原因。(Marinescu 2006)
  • 最后,Lionel d'Hauenens(http://www.laboskopia.com)的`Symbol Type Viewer是我分析Windows 7堆管理器使用的数据结构时使用的一个宝贵的工具。如果没有它,可能会浪费大量时间来寻找数据结构`。

Prerequisites(预备知识)

  除非另有说明,否则本文中使用的所有伪代码结构体均源自32位Windows 7ntdll.dll,版本为6.1.7600.16385。结构体定义是通过Symbol Type ViewerWindbgMicrosoft Symbol Server中下载的库中获得的。

  为了简洁起见,已对该代码的伪代码表示进行了大量修改,以便将精力集中在最常用的堆管理算法上。如果您觉得我遗漏了一些东西对代码有错误的理解,请通过cvalasek@gmail.com与我联系。我会给你奖金。

Terminology(术语)

  关于Windows堆的文章很多,不幸的是,我在资料中看到了很多不同的术语。尽管本文中使用的术语可能与其他人的也不同,但为了在本文档中保持一致,我现在要对其进行定义

  术语 “block”“blocks” 表示8字节的连续内存。这是堆块(heap chunk)头在引用大小时使用的度量单位“chunk” 是一块连续的内存,可以以“blocks”“bytes”为单位进行度量。

  “HeapBase” 是Windows调试符号所定义的“_HEAP”结构的指针的别名。在本文中,“objects”都会按“HeapBase”起始的某个偏移量进行定义。同时,“低碎片堆(Low Fragmentation Heap)”将缩写为 “LFH”

  “BlocksIndex”“_HEAP_LIST_LOOKUP”结构的别名。这两个术语可以相互替代“BlocksIndex”结构体通过Lists来管理“chunks”,管理0x400(1024)字节及以下的“chunks”Lists被称为 1st BlocksIndex ,而管理0x400-0x4000(16k)字节“chunks”Lists被称为 2nd BlocksIndex。大于16k且在DeCommitThreshold0xFE00“blocks”(VirtualMemoryThreshold)以下的“chunks”将以类似于 FreeList[0] 的结构进行管理(本文稍后讨论)。

  专用“FreeLists”的概念已经消失。术语 “ListHint”“FreeList” 用来表示指向Heap->FreeLists链表中特定位置的一个链表。这将在本文的“Architecture(架构)”部分进行展开。

  最后,当指代从“低碎片堆(Low Fragmentation Heap)”分配特定大小的内存时,将使用术语 “HeapBin”“Bin”“UserBlock”。我知道大多数人都将其称为“HeapBucket”,但是为了避免造成混淆,我将避免这样做,这是为了避免与微软调试符号“_HEAP_BUCKET”产生混淆,“_HEAP_BUCKET”是一个0x4字节的数据结构,用来指定一个大小而不是用于内存容器

Notes(说明)

  本文旨在作为John McDonald和我Blackhat USA 2009完成的工作的后续知识。有关双链表,Lookaside链表等结构的内部工作原理的知识,请参见标题为 “Practical Windows XP/2003 Exploitation” 的论文。(John McDonald/Chris Valasek 2009)

Data Structures(数据结构)

  这些数据结构源自版本为6.1.7600.16385(SP0)ntdll.dllWindows调试符号。它们在堆管理器中被用于跟踪内存,从而通过抽象的函数调用为用户提供对虚拟内存的无缝访问。主要是HeapAlloc()HeapFree()malloc()free()

_HEAP

_HEAP(HeapBase)

  每个创建的堆都以一个称为“HeapBase”的必要结构开始。“HeapBase”包含堆管理器所用到的多个重要的值结构体指针。这是每个堆的心脏,为了提供可靠的分配和释放操作,必须保持其结构的完整性。如果您熟悉Windows XP代码库中使用的“HeapBase”,这将看起来非常相似。但其中某些加粗的字段仍需要进一步解释。下面显示了32位Windows 7 Service Pack 0“_HEAP”结构的内容:

Listing 1. _HEAP via windbg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
0:001> dt _HEAP
ntdll!_HEAP
+0x000 Entry : _HEAP_ENTRY ; 用于存放管理结构的堆块结构
+0x008 SegmentSignature : Uint4B ; 段签名,固定为0xffeeffee
+0x00c SegmentFlags : Uint4B ; 段标志
+0x010 SegmentListEntry : _LIST_ENTRY ;
+0x018 Heap : Ptr32 _HEAP ; _HEAP结构指针,表明此_HEAP属于哪个堆
+0x01c BaseAddress : Ptr32 Void ; 此堆的基地址
+0x020 NumberOfPages : Uint4B ; 此堆中的页数量,页面大小为4k
+0x024 FirstEntry : Ptr32 _HEAP_ENTRY ; Segment0的第一个_HEAP_ENTRY结构指针
+0x028 LastValidEntry : Ptr32 _HEAP_ENTRY ; Segment0的最后一个_HEAP_ENTRY结构指针
+0x02c NumberOfUnCommittedPages : Uint4B ; 未提交的页数量
+0x030 NumberOfUnCommittedRanges : Uint4B ; 未提交的范围数量
+0x034 SegmentAllocatorBackTraceIndex : Uint2B ;
+0x036 Reserved : Uint2B ; 保留
+0x038 UCRSegmentList : _LIST_ENTRY ; UCR=UnCommitedRange
+0x040 Flags : Uint4B ; 堆标志,2代表HEAP_GROWABLE
+0x044 ForceFlags : Uint4B ; 强制标志
+0x048 CompatibilityFlags : Uint4B ; 兼容性标志,与LFH激活有关的是0x20000000
+0x04c EncodeFlagMask : Uint4B ;<----
+0x050 Encoding : _HEAP_ENTRY ;<----
+0x058 PointerKey : Uint4B ;
+0x05c Interceptor : Uint4B ;
+0x060 VirtualMemoryThreshold : Uint4B ; 最大堆块大小(以分配粒度为单位),可以在段中分配的堆块最大值,以用户数据区的大小来衡量
+0x064 Signature : Uint4B ; _HEAP结构的签名,固定为0xeeffeeff
+0x068 SegmentReserve : Uint4B ; 段的保留空间大小(以字节为单位),未设置则默认为0x100000 = 1MB
+0x06c SegmentCommit : Uint4B ; 每次提交内存的大小(以字节为单位),未设置则默认为0x2000 = 8KB = PAGE_SIZE*2
+0x070 DeCommitFreeBlockThreshold : Uint4B ; 解除提交的单块阈值(以粒度为单位)
+0x074 DeCommitTotalFreeThreshold : Uint4B ; 解除提交的总空闲空间阈值(以粒度为单位)
+0x078 TotalFreeSize : Uint4B ; 空闲空间总大小(以粒度为单位)
+0x07c MaximumAllocationSize : Uint4B ; 可分配的最大值(以字节为单位),MmHighestUserAddress = 0x7ffdefff
+0x080 ProcessHeapsListIndex : Uint2B ; 本堆在进程堆列表中的索引
+0x082 HeaderValidateLength : Uint2B ; 头结构的有效长度(_HEAP)
+0x084 HeaderValidateCopy : Ptr32 Void ;
+0x088 NextAvailableTagIndex : Uint2B ; 下一个可用的堆块标记索引
+0x08a MaximumTagIndex : Uint2B ; 最大的堆块标记索引
+0x08c TagEntries : Ptr32 _HEAP_TAG_ENTRY ; 指向用于标记堆块的标记结构
+0x090 UCRList : _LIST_ENTRY ; UCR=UnCommitedRange
+0x098 AlignRound : Uint4B ;
+0x09c AlignMask : Uint4B ; 用于地址对齐的掩码
+0x0a0 VirtualAllocdBlocks : _LIST_ENTRY ; 虚拟分配的块的链表(VirtualAlloc)
+0x0a8 SegmentList : _LIST_ENTRY ; 段链表
+0x0b0 AllocatorBackTraceIndex : Uint2B ; 用于记录回溯信息
+0x0b4 NonDedicatedListLength : Uint4B ;
+0x0b8 BlocksIndex : Ptr32 Void ;<----
+0x0bc UCRIndex : Ptr32 Void ; UCR=UnCommitedRange
+0x0c0 PseudoTagEntries : Ptr32 _HEAP_PSEUDO_TAG_ENTRY ;
+0x0c4 FreeLists : _LIST_ENTRY ;<----
+0x0cc LockVariable : Ptr32 _HEAP_LOCK ; 用于串行化控制的同步对象
+0x0d0 CommitRoutine : Ptr32 long ;
+0x0d4 FrontEndHeap : Ptr32 Void ;<----前端堆(LFH)
+0x0d8 FrontHeapLockCount : Uint2B ; 前端堆的锁定计数
+0x0da FrontEndHeapType : UChar ;<----前端堆的类型
+0x0dc Counters : _HEAP_COUNTERS ;
+0x130 TuningParameters : _HEAP_TUNING_PARAMETERS ;
  • EncodeFlagMask - 一个用于确定堆块头(Heap Chunk Header)是否已编码的值。该值最初由RtlCreateHeap()中的RtlpCreateHeapEncoding()设置为0x100000
  • Encoding - 在XOR操作中用于对块头(Heap Chunk Header)进行编码,以防止可预测的元数据损坏。
  • BlocksIndex - 这是一个_HEAP_LIST_LOOKUP结构,可用于多种用途。由于其重要性,将在本文档的后面部分对此进行更详细的讨论。
  • FreeLists - 一个特殊的链接,包含了此堆上的所有“Free Chunk”的指针。几乎可以将其视为“Heap Cache”,但是适用于各种大小的Chunks(并且没有单个关联的bitmap)。
  • FrontEndHeap - 指向关联的前端堆(Front-End Heap)的指针。在Windows 7下,它可以为“NULL”或指向“_LFH_HEAP”结构的指针。
  • FrontEndHeapType - 初始化设置为0x0的整型数,随后会被赋值为0x2,指示LFH被使用。注意:Windows 7实际上不支持Lookaside链表。

_HEAP_LIST_LOOKUP

_HEAP_LIST_LOOKUP(HeapBase->BlocksIndex)

  理解_HEAP_LIST_LOOKUP结构是建立Windows 7堆管理的坚实基础的最重要任务之一。这是后端堆管理器(Back-End Manager)和前端堆管理器(Front-End Manager)使用分配和释放的基石。在正常情况下,在RtlCreateHeap()中初始化的1st _HEAP_LIST_LOOKUP结构将位于HeapBase+0x150的位置。

Listing 2. _HEAP_LIST_LOOKUP via windbg

1
2
3
4
5
6
7
8
9
10
11
0:001> dt _HEAP_LIST_LOOKUP
ntdll!_HEAP_LIST_LOOKUP
+0x000 ExtendedLookup : Ptr32 _HEAP_LIST_LOOKUP <----
+0x004 ArraySize : Uint4B <----
+0x008 ExtraItem : Uint4B
+0x00c ItemCount : Uint4B
+0x010 OutOfRangeItems : Uint4B <----
+0x014 BaseIndex : Uint4B <----
+0x018 ListHead : Ptr32 _LIST_ENTRY <----
+0x01c ListsInUseUlong : Ptr32 Uint4B <----
+0x020 ListHints : Ptr32 Ptr32 _LIST_ENTRY <----
  • ExtendedLookup - 指向下一个_HEAP_LIST_LOOKUP结构的指针。如果没有下一个,则该值为NULL。
  • ArraySize - 此结构可以跟踪的最大block的大小,超出此大小的Chunk则将其存储在特殊的ListHint中。Windows 7当前唯一使用的两种大小是0x80和0x800。
  • OutOfRangeItems - 这个4字节的值记载了类似FreeList[0]的结构中的条目数。每个_HEAP_LIST_LOOKUP会通过ListHint [ArraySize-BaseIndex-1]来跟踪大于ArraySize-1的空闲块(Free Chunks)。
  • BaseIndex - 用于索引ListHints数组的相对偏移量,每个_HEAP_LIST_LOOKUP被设计成对应于某一个具体大小。例如,1st BlocksIndex的BaseIndex为0x0,因为它管理的Chunk的大小范围为0x0~0x80,而2nd BlocksIndex的BaseIndex为0x80。
  • ListHead - 它与HeapBase->FreeLists指向相同的位置,该位置是一个链表,存储了堆中可用的所有的“Free Chunks”。
  • ListsInUseUlong - 形式上作为FreeListInUseBitmap,此4字节整型数是一种优化,用于判断哪些ListHint具有可用的Chunk。
  • ListHints - 也称为FreeLists,这些链表提供了指向Free Chunk的指针,同时还具有其他目的。如果为给定的Bucket大小启用了LFH,则特定大小的ListHint/FreeList的blink将指向_HEAP_BUCKET+1(_HEAP_BUCKET结构地址加1)。

_LFH_HEAP

_LFH_HEAP(HeapBase->FrontEndHeap)

  LFH由此数据结构管理。当LFH被激活后,它将使堆管理器知道它能够管理什么大小,同时对此前用过的Chunks保持缓存。尽管BlocksIndex能够跟踪大小超过0x800 blocks的Chunks,但是LFH仅用于小于16k的Chunks。

Listing 3. _LFH_HEAP via windbg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0:001> dt _LFH_HEAP
ntdll!_LFH_HEAP
+0x000 Lock : _RTL_CRITICAL_SECTION
+0x018 SubSegmentZones : _LIST_ENTRY
+0x020 ZoneBlockSize : Uint4B
+0x024 Heap : Ptr32 Void <----
+0x028 SegmentChange : Uint4B
+0x02c SegmentCreate : Uint4B
+0x030 SegmentInsertInFree : Uint4B
+0x034 SegmentDelete : Uint4B
+0x038 CacheAllocs : Uint4B
+0x03c CacheFrees : Uint4B
+0x040 SizeInCache : Uint4B
+0x048 RunInfo : _HEAP_BUCKET_RUN_INFO
+0x050 UserBlockCache : [12] _USER_MEMORY_CACHE_ENTRY <----
+0x110 Buckets : [128] _HEAP_BUCKET <----
+0x310 LocalData : [1] _HEAP_LOCAL_DATA <----
  • Heap - 指向此LFH父堆的指针。
  • UserBlockCache - 尽管不会进行详细讨论该字段,但值得一提的是,UserBlockCache数组会跟踪那些先前使用过的内存块,以供将来分配。
  • Buckets - 0x4字节数据结构的数组,仅用于跟踪索引和大小。这就是为什么术语“Bin”将被用来描述满足特定Bucket请求的内存区域的原因。
  • LocalData - 这是指向大型数据结构的指针,该数据结构保存了每个SubSegment的信息。有关更多信息,请参见_HEAP_LOCAL_DATA。

_HEAP_LOCAL_DATA

_HEAP_LOCAL_DATA(HeapBase->FrontEndHeap->LocalData)

  为LFH提供_HEAP_LOCAL_SEGMENT_INFO实例的关键结构。

Listing 4. _HEAP_LOCAL_DATA via windbg

1
2
3
4
5
6
7
0:000> dt _HEAP_LOCAL_DATA
ntdll!_HEAP_LOCAL_DATA
+0x000 DeletedSubSegments : _SLIST_HEADER
+0x008 CrtZone : Ptr32 _LFH_BLOCK_ZONE
+0x00c LowFragHeap : Ptr32 _LFH_HEAP <----
+0x010 Sequence : Uint4B
+0x018 SegmentInfo : [128] _HEAP_LOCAL_SEGMENT_INFO <----
  • LowFragHeap - 与该结构关联的LFH。
  • SegmentInfo - _HEAP_LOCAL_SEGMENT_INFO结构的数组,表示此LFH的所有可用大小。有关更多信息,请参见_HEAP_LOCAL_SEGMENT_INFO。

_LFH_BLOCK_ZONE

_LFH_BLOCK_ZONE(HeapBase->FrontEndHeap->LocalData->CrtZone)

  该数据结构用于跟踪那些用于服务分配请求的内存的位置。这些指针是在LFH服务第一个请求时,或者在指针列表用完之后被设置。

Listing 5. _LFH_BLOCK_ZONE via windbg

1
2
3
4
5
0:000> dt _LFH_BLOCK_ZONE
ntdll!_LFH_BLOCK_ZONE
+0x000 ListEntry : _LIST_ENTRY <----
+0x008 FreePointer : Ptr32 Void <----
+0x00c Limit : Ptr32 Void <----
  • ListEntry - _LFH_BLOCK_ZONE结构的链表。
  • FreePointer - 一个指向可以被_HEAP_SUBSEGMENT使用的内存指针。
  • Limit - 链表中的最后一个_LFH_BLOCK_ZONE结构的指针。当达到或超过此值时,后端堆(Back-End Heap)将会创建更多的_LFH_BLOCK_ZONE结构。

_HEAP_LOCAL_SEGMENT_INFO

_HEAP_LOCAL_SEGMENT_INFO(HeapBase->FrontEndHeap->LocalData->SegmentInfo[])

  要服务的请求的大小将确定使用哪一个_HEAP_LOCAL_SEGMENT_INFO结构。该结构保存了堆算法在确定最有效的分配和释放内存的方式时使用的信息。尽管_HEAP_LOCAL_DATA中只有128个该结构,但是所有小于16k的8字节对齐的大小都具有对应的_HEAP_LOCAL_SEGMENT_INFO结构。有一种特殊的算法用于计算相对索引,从而确保每个Bucket都具有专用的结构。

Listing 6. _HEAP_LOCAL_SEGMENT_INFO via windbg

1
2
3
4
5
6
7
8
9
10
11
0:000> dt _HEAP_LOCAL_SEGMENT_INFO
ntdll!_HEAP_LOCAL_SEGMENT_INFO
+0x000 Hint : Ptr32 _HEAP_SUBSEGMENT <----
+0x004 ActiveSubsegment : Ptr32 _HEAP_SUBSEGMENT <----
+0x008 CachedItems : [16] Ptr32 _HEAP_SUBSEGMENT
+0x048 SListHeader : _SLIST_HEADER
+0x050 Counters : _HEAP_BUCKET_COUNTERS
+0x058 LocalData : Ptr32 _HEAP_LOCAL_DATA <----
+0x05c LastOpSequence : Uint4B
+0x060 BucketIndex : Uint2B <----
+0x062 LastUsed : Uint2B
  • Hint - 仅当LFH释放正在管理的Chunk时,才设置此SubSegment。如果从不释放块,则该值将始终为NULL。
  • ActiveSubsegment - 用于大多数内存请求的SubSegment。初始化为NULL,当为某个特定大小进行第一次分配时设置。
  • LocalData - 与此结构关联的_HEAP_LOCAL_DATA结构指针。
  • BucketIndex - 每个SegmentInfo对象都与一个具体的Bucket大小(或索引)相关。

_HEAP_SUBSEGMENT

_HEAP_SUBSEGMENT(HeapBase->FrontEndHeap->LocalData->SegmentInfo[]->Hint, ActiveSubsegment, CachedItems)

  在为特定的_HEAP_BUCKET确定适当的结构后,前端堆管理器(Front-End Manager)将执行释放或分配。由于可以将LFH视为堆管理器中的堆管理器,因此使用_HEAP_SUBSEGMENT来跟踪还有多少内存可用以及应该如何分布是有意义的。

Listing 7. _HEAP_SUBSEGMENT via windbg

1
2
3
4
5
6
7
8
9
10
11
12
13
0:000> dt _HEAP_SUBSEGMENT
ntdll!_HEAP_SUBSEGMENT
+0x000 LocalInfo : Ptr32 _HEAP_LOCAL_SEGMENT_INFO <----
+0x004 UserBlocks : Ptr32 _HEAP_USERDATA_HEADER <----
+0x008 AggregateExchg : _INTERLOCK_SEQ <----
+0x010 BlockSize : Uint2B
+0x012 Flags : Uint2B
+0x014 BlockCount : Uint2B
+0x016 SizeIndex : UChar <----
+0x017 AffinityIndex : UChar
+0x010 Alignment : [2] Uint4B
+0x018 SFreeListEntry : _SINGLE_LIST_ENTRY
+0x01c Lock : Uint4B
  • LocalInfo - 与此结构关联的_HEAP_LOCAL_SEGMENT_INFO结构。
  • UserBlocks - 与此SubSegment耦合的_HEAP_USERDATA_HEADER结构,它保存一个被分割成n个Chunk的大的内存Chunk。
  • AggregateExchg - _INTERLOCK_SEQ结构,用于跟踪当前的Offset和Depth。
  • SizeIndex - 此SubSegment的_HEAP_BUCKET SizeIndex。

_HEAP_USERDATA_HEADER

_HEAP_USERDATA_HEADER(HeapBase->FrontEndHeap->LocalData->SegmentInfo[]->Hint, ActiveSubsegment, CachedItems->UserBlocks)

  此头位于UserBlock Chunk之前,该Chunk用于为LFH的所有请求提供服务。在执行所有逻辑以找到一个SubSegment之后,已提交(committed)内存实际上操纵的位置就是该结构。

Listing 8. _HEAP_USERDATA_HEADER via windbg

1
2
3
4
5
6
7
0:000> dt _HEAP_USERDATA_HEADER
ntdll!_HEAP_USERDATA_HEADER
+0x000 SFreeListEntry : _SINGLE_LIST_ENTRY
+0x000 SubSegment : Ptr32 _HEAP_SUBSEGMENT
+0x004 Reserved : Ptr32 Void
+0x008 SizeIndex : Uint4B
+0x00c Signature : Uint4

_INTERLOCK_SEQ

_INTERLOCK_SEQ(HeapBase->FrontEndHeap->LocalData->SegmentInfo[]->Hint, ActiveSubsegment, CachedItems->AggregateExchg)

  由于UserBlock Chunk的被划分方式,需要有一种方法来获取当前Offset,用于释放或分配下一个块。该过程由_INTERLOCK_SEQ数据结构控制。

Listing 9. _INTERLOCK_SEQ via windbg

1
2
3
4
5
6
7
0:000> dt _INTERLOCK_SEQ
ntdll!_INTERLOCK_SEQ
+0x000 Depth : Uint2B <----
+0x002 FreeEntryOffset : Uint2B <----
+0x000 OffsetAndDepth : Uint4B <----
+0x004 Sequence : Uint4B
+0x000 Exchg : Int8B
  • Depth - 一个计数器,用于跟踪UserBlock中还剩下多少个Chunk。释放时该值会递增,分配时则递减。它的值初始化为UserBlock的大小除以HeapBucket的大小。
  • FreeEntryOffset - 此2字节整型数保存一个值,当将其与_HEAP_USERDATA_HEADER的地址相加时,将返回指向下一个释放或分配内存的位置的指针。该值以blocks(0x8字节Chunk)表示,并被初始化为0x2,因为sizeof(_HEAP_USERDATA_HEADER)等于0x10。[0x2 * 0x8 == 0x10]
  • OffsetAndDepth - 由于Depth和FreeEntryOffset均为2个字节,所以它们可以组合成这个4字节的值。(译者注:注意这是个union)

_HEAP_ENTRY

_HEAP_ENTRY(Chunk Header)

  _HEAP_ENTRY,也称为堆块头(Heap Chunk Header),是一个8字节的值,存储在堆中每个内存Chunk之前(即使是UserBlocks内部的Chunk,也一样)。由于新版本Windows在Header中引入了有效性和安全性的修改,所以自Windows XP基础代码以来,它已发生了巨大变化。

Listing 10. _HEAP_ENTRY via Windbg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0:001> dt _HEAP_ENTRY
ntdll!_HEAP_ENTRY
+0x000 Size : Uint2B <----
+0x002 Flags : UChar <----
+0x003 SmallTagIndex : Uchar <----
+0x000 SubSegmentCode : Ptr32 Void
+0x004 PreviousSize : Uint2B
+0x006 SegmentOffset : Uchar
+0x006 LFHFlags : Uchar
+0x007 UnusedBytes : Uchar <----
+0x000 FunctionIndex : Uint2B
+0x002 ContextValue : Uint2B
+0x000 InterceptorValue : Uint4B
+0x004 UnusedBytesLength : Uint2B
+0x006 EntryOffset : Uchar
+0x007 ExtendedBlockSignature : Uchar <----
  • Size - Chunk的大小(以blocks为单位),这包括_HEAP_ENTRY本身。
  • Flags - 指示此堆块状态的标志。比如“free”或“busy”。
  • SmallTagIndex - 该值存储_HEAP_ENTRY前三个字节的XOR校验值。
  • UnusedBytes/ExtendedBlockSignature - 表示未使用的字节(保留待以后使用),或是一个指示被LFH管理的Chunk的状态的字节。

Overview(概览)

Data structure overview

Diagram 1. Data structure overview

Architecture(架构)

  自Windows XP时代以来,至Windows 7,堆管理器已经发生了翻天覆地的变化,因此重温一些简洁的架构调整将很有必要。特别的,
FreeLists工作的方式进行了重构,数据是如何存储于其中也需要进一步解释。

FreeLists(WinXP&Win7)

  在我们讨论核心算法之前,必须调查下当前和以前的FreeList结构。这是因为FreeLists的操作和存储数据的方式自Windows XP基础代码以来发生了改变。这是John McDonald和我曾在此前的一篇论文中给出FreeList结构的概述:

Windows XP

  每个可能的块大小(小于1024字节)都有单独的链表,总共有128个空闲链表(FreeLists)(堆块的大小为8的倍数)。每个双向空闲链表都有一个哨兵头节点,存储于堆基址处某偏移的数组中。每个头节点包含两个指针:一个前向指针(FLink)和一个后向指针(BLink)。FreeList[1]没有被用到(这句有点问题),而FreeList[2]-FreeList[127]被称为专用的空闲链表(Dedicated Free Lists)。对于这些专用链表,链表中所有空闲块的大小均相同,大小应该是数组索引*8。但是,所有大于或等于1024字节的块都保存在单个空闲链表FreeList[0]中(此槽是可用的,因为没有任何大小为0的空闲块。)。此链表中的空闲块按最小的块到最大的块升序排列。因此,FreeList[0].Flink指向最小的空闲块(Size>=1024),而FreeList[0].Blink指向最大的空闲块(Size>=1024)。
(Windows XP SP3,Windows Server 2003)

Windows XP FreeList Relationships

Diagram 2. Windows XP FreeList relationships

Windows 7

  由于LFH更改了前端堆管理器(Front-End Manager)的工作方式,因此后端堆管理器(Back-End Manager)也必须进行适配。此后不再有一个专用的FreeList,取而代之的,每个BlocksIndex都有它自己的ListHints,初始化为NULL。

  BlocksIndex结构包含指向它自己的ListHints的指针,而ListHints指向FreeLists结构。它的设置与旧版本的FreeLists非常相似,只不过FreeList[0]不再用作存储大于0x400(1024)字节的Chunk,而是有条件的。如果没有BlocksIndex->​​ExtendedLookup,则所有大小大于或等于BlocksIndex->​​ArraySize-1的块都将以升序存储在FreeList[ArraySize-BaseIndex–1]中。

  尽管FreeLists包含哨兵节点,该节点在以ListHints指针计算的某个偏移位置处,而ListHints指针是大部分相似节点结束的地方。虽然Flink指针仍然指向FreeLists的下一个可用的Chunk,但它也可以扩展到更大的FreeLists。这使得Heap.FreeLists可以遍历特定堆的每一个可用的空闲Chunk。

  哨兵节点的Blink也做了调整,为了满足两个目的。如果未为Bucket启用LFH,那么哨兵Blink将作为启发式分配的计数。否则,它将存储_HEAP_BUCKET+1的地址(除了ListHint[ArraySize-BaseIndex-1]这种情况)。

  下面的图是一个稀疏填充堆的实例,展示了这些新的结构体之间是如何交互的。它包含了一个用于跟踪1024字节以下大小的Chunk的BlocksIndex。与此堆关联的Chunk仅有5个,可以通过各种方式访问它们。

  例如,如果请求分配0x30(48)字节,堆将尝试使用ListHint[0x6]。你可以看到,尽管只有3个大小为0x30的空闲Chunk,但最后一个大小为0x30的空闲Chunk的Flink指向一个属于ListHint[0x7]的条目。ListHint[0x7]只有一个条目,但和ListHint[0x6]一样的是,它的最后一个Chunk指向一个超出大小边界的更大的Chunk。

  这改变了链表终止的方式。链表中的最后一个节点不再指向所在的FreeList的哨兵节点,而是指向HeapBase+0xC4处的FreeLists条目。

注意:_HEAP_LIST_LOOKUP结构在RtlCreateHeap()或RtlpExtendListLookup()中初始化时,ListHead设置为指向Heap.FreeLists(HeapBase+0xC4)。这使两个条目相同,并指向内存中的同一区域。

New FreeList relationship

Diagram 3. New FreeList relationship

Algorithms(算法)

  要充分理解堆的确定性和漏洞利用理论,必须奠定基本的知识基础。没有这些核心知识,就只能默念“大神保佑”。本节将把核心算法分成两个部分:分配和释放,并分为后端堆管理器和前端堆管理器两种情形。之所以如此,是因为前端堆管理器和后端堆管理器执行的内存操作可能会影响另一端的状态。

Allocation(分配)

  当试图服务来自调用应用程序的请求时,分配会从RtlAllocateHeap()开始。该函数首先会以8字节对齐分配量。此后,它会获取一个ListHints的索引。如果没有找到特定索引,就使用BlocksIndex->ArraySize-1。

Listing 11. RtlAllocateHeap BlocksIndex Search

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if(Size == 0x0)
Size = 0x1;

//ensure that this number is 8‐byte aligned
//保证分配量是8字节对齐
int RoundSize = Round(Size);

int BlocksSize = Size/8;

//get the HeapListLookup, which determines if we should use the LFH
//获取HeapListLookup,判断是否该使用LFH
_HEAP_LIST_LOOKUP *BlocksIndex = (_HEAP_LIST_LOOKUP*)heap‐>BlocksIndex;

//loop through the HeapListLookup structures to determine which one to use
//遍历HeapListLookup结构,找出需要使用的是哪一个
while(BlocksSize >= BlocksIndex‐>ArraySize)
{
if(BlocksIndex‐>ExtendedLookup == NULL)
{
BlocksSize = BlocksIndex‐>ArraySize‐1;
break;
}
BlocksIndex = BlocksIndex‐>ExtendedLookup;
}

  有一种情况会返回BlocksIndex->ArraySize-1作为ListHint索引。如果出现了这种情况,那么后端分配器会使用一个值为NULL的FreeList。这将引起后端分配器尝试使用Heap->FreeLists。如果FreeLists不包含大小充足的Chunk,堆会使用RtlpExtendHeap()来进行扩展。

  如果特定的索引被成功获取到,那么堆管理器会试图使用FreeList来满足需求的尺寸。它会根据FreeList->Blink来判断对该Bucket来说LFH是否有激活;如果没有的话,堆管理器会默认使用后端堆管理器:

Listing 12. RtlAllocateHeap heap manager selector

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//get the appropriate freelist to use based on size
//基于大小获取对应的FreeList
int FreeListIndex = BlocksSize ‐ HeapListLookup‐>BaseIndex;

_LIST_ENTRY *FreeList = &HeapListLookup‐>ListHints[FreeListIndex];
if(FreeList)
{
//check FreeList[index]‐>Blink to see if the heap bucket context has been populated via RtlpGetLFHContext()
//RtlpGetLFHContext() stores the HeapBucket context + 1 in the Blink
//检查FreeList[index]‐>Blink,看看heap bucket context是否由RtlpGetLFHContext()填充过
//RtlpGetLFHContext()在Blink中存储了HeapBucket context + 1
_HEAP_BUCKET *HeapBucket = FreeList‐>Blink;

if(HeapBucket & 1)
{
RetChunk = RtlpLowFragHeapAllocFromContext(HeapBucket‐1, aBytes);

if(RetChunk && heap‐>Flags == HEAP_ZERO_MEMORY)
memset(RetChunk, 0, RoundSize);
}
}

//if the front‐end allocator did not succeed, use the back‐end
//如果前端分配器没有成功分配,那就用后端分配器
if(!RetChunk)
{
RetChunk = RtlpAllocateHeap(heap, Flags | 2, Size, RoundSize, FreeList);
}

Back-end Allocation(后端分配器)

  后端分配器(Back-end Allocation)是分配算法的最后一道防线,如果它失败了,那么内存请求失败返回NULL。除了为无法由前端分配器(Front-end Allocation)服务的内存请求提供服务这一职责以外,后端分配器还负责启发式激活(Activation Heuristics)前端分配器(Front-end Allocation)。它的工作方式和Windows XP基础代码中的启发式堆缓存(Heap Cache Heuristic)的工作方式非常相似。

RtlpAllocateHeap

  _HEAP结构体,要分配的大小以及期望的ListHint(FreeList)作为一部分参数传递给RtlpAllocateHeap()。如同RtlAllocateHeap般,第一步就是对待分配的大小按8字节对齐,同时还要判断Flags是否对HEAP_NO_SERIALIZE置位。如果该位置位,则LFH不会启用。(http://msdn.microsoft.com/enus/library/aa366599%28v=VS.85%29.aspx)

Listing 13. RtlpAllocateHeap size rounding and Heap maintenance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
int RoundSize;

//if the FreeList isn't NULL, the rounding has already been preformed
//如果FreeList不是NULL,那么就已经完成大小对齐了
if(FreeList)
{
RoundSize = RoundSize;
}
else
{
int MinSize = 1;
if(Size)
MinSize = Size;

//rounds to the nearest 8‐byte aligned number
//向上取舍到最近的8字节对齐大小
RoundSize = Round(MinSize);
}

int SizeInBlocks = RoundSize/8;

if(SizeInBlocks < 2)
{
//RoundSize += sizeof(_HEAP_ENTRY)
RoundSize = RoundSize + 8;
SizeInBlocks = 2;
}

//if NOT HEAP_NO_SERIALIZE, use locking mechanisms
//LFH CANNOT be enabled if this path isn't taken
//如果没有设置HEAP_NO_SERIALIZE,就使用锁定机制
//如果没有使用此路径,则无法启用LFH
if(!(Flags & HEAP_NO_SERIALIZE))
{
//setup locking mechanisms here
//设置锁定机制

//if we have certain compaitibility flags(either set below, or otherwise,
//then we will call 'RtlpPerformHeapMaintenance'
//which will activate the LFH and setup an ExtendedListLookup as well
//如果有具体的兼容性标志,就会调用'RtlpPerformHeapMaintenance'
//它会激活LFH,也会设置ExtendedListLookup
if(Heap‐>CompatibilityFlags & 0x60000000)
RtlpPerformHeapMaintenance(Heap);
}

注意:你可以在后面看到CompatibilityFlags是如何在后续代码中设置的。这就是LFH被激活的方式。尽管LFH是默认前端堆管理器,但直到具体的启发式策略被触发之前,它实际上并不进行任何的内存管理。

  即使此时LFH可能已准备好为请求提供服务,但后端分配器仍将继续进行此分配。通过省略用于处理虚拟内存请求的代码,可以看到RtlpAllocateHeap()将尝试查看FreeList参数是否为非NULL。根据到来的有效的FreeList参数,后端管理器会应用启发式机制来判断是否应将LFH用于以后的任何分配:

Listing 14. RtlpAllocateHeap LFH Heuristic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
if(FreeList != NULL)
{
//if this freelist doesn't hold a _HEAP_BUCKET, update the counters and attempt to get the LFH context
//如果freelist未拥有一个_HEAP_BUCKET,就更新计数器并试图获取LFH上下文
if(!(FreeList‐>Blink & 1)) //未启用LFH
{
//add a certain amount to the blink
//为blink增加一个具体的数量
FreeList‐>Blink += 0x10002;

//if the counter has ran more than 0x10 times, OR
//we haven't successfully entered the critical section, OR we've been through 0x1000 iterations
//then attempt to set the Compatibility flags(which, in turn, will call RtlpPerformHeapMaintenance())
//如果计数器已经执行了超过0x10次,或者我们不曾成功进入到关键部分,或者我们已经进行了0x1000次迭代
//那么就会试图设置这个Compatibility标志(这就意味着我们会调用到RtlpPerformHeapMaintenance())
if((WORD)FreeList‐>Blink > 0x20 || FreeList‐>Blink > 0x10000000)
{
//if the FrontEndHeapType is LFH (0x2) assign it
//如果FrontEndHeapType是LFH(0x2),进行赋值
int FrontEndHeap;
if(Heap‐>FrontEndHeapType == 0x2)
FrontEndHeap = Heap‐>FrontEndHeap;
else
FrontEndHeap = NULL;

//this function gets a _HEAP_BUCKET, stored in _LFH_HEAP‐>Bucket[BucketSize]
//if the LFH hasn't been activated yet, it will return NULL
//该函数获取一个_HEAP_BUCKET,它存储于_LFH_HEAP‐>Bucket[BucketSize]
//如果LFH仍未被激活,就返回NULL
char *LFHContext = RtlpGetLFHContext(FrontEndHeap, Size);

//if the context isn't set and we've seen 0x10+ allocations, set the flags
//如果上下文没有设置并且我们已经进行了0x10+次分配,也要设置该标志
if(LFHContext == NULL)
{
if((WORD)FreeList‐>Blink > 0x20)
{
//RtlpPerformHeapMaintenance heurstic
//RtlpPerformHeapMaintenance启发式机制
if(Heap‐>FrontEndHeapType == NULL)
Heap‐>CompatibilityFlags |= 0x20000000;
}
}
else
{
//save the _HEAP_BUCKET in the Blink
//+1 == _HEAP_BUCKET
//保存_HEAP_BUCKET到Blink
FreeList‐>Blink = LFHContext + 1;
}
}
}
}

注意:这就是我为什么一直说前端和后端堆管理器存在紧密联系的原因。如你所见ListHint用来存储某个_HEAP_BUCKET的地址,它用来判断管理器是否应该使用LFH。这一双重用法看起来有点困惑,但是在讨论过前端分配和释放算法之后,它将变得非常清晰。

  现在已经设置了LFH激活标志,分配可以在后端继续进行了。检查FreeList以查看是否已填充,然后执行Safe Unlink检查。这样可以确保FreeList值保持其完整性,以防止在Unlinking时被4字节覆盖所利用。ListsInUseUlong(FreeListInUseBitmap)随后会相应地更新。最后,从链表上卸下来的Chunk会更新头部,转为BUSY态并返回。

Listing 15. RtlpAllocateHeap ListHint allocation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
//attempt to use the Flink
//试图使用Flink
if(FreeList != NULL && FreeList‐>Flink != NULL)
{
int SizeToUse;

//saved values,保存值
_HEAP_ENTRY *Blink = FreeList‐>Blink;
_HEAP_ENTRY *Flink = FreeList‐>Flink;

//get the heap chunk header by subtracting 8
//通过减去8来获取堆头
_HEAP_ENTRY *ChunkToUseHeader = Flink ‐ 8;

//decode the header if applicable
//解码头部(如果适用)
if(Heap‐>EncodeFlagMask)
DecodeAndValidateChecksum(ChunkToUseHeader);

//ensure safe unlinking before acquiring this chunk for use
//在获取该块以供使用之前,确保安全解除链接
if(Blink‐>Flink != Flink‐>Blink || Blink‐>Flink != FreeList)
{
RtlpLogHeapFailure();
//XXX RtlNtStatusToDosError and return
}

//decrement the total heap size
//减小整体堆大小
Heap‐>TotalFreeSize ‐= ChunkToUseHeader‐>Size;

//iterate through the BlocksIndex structures
//If no sufficient BlocksIndex is found, use BlocksIndex‐>ArraySize ‐ 1
//迭代BlocksIndex结构体,如果找不到充足的BlocksIndex,就用BlocksIndex‐>ArraySize ‐ 1
_HEAP_LIST_LOOKUP *BlocksIndex = Heap‐>BlocksIndex;
if(BlocksIndex)
{
int ListHintIndex = GetListHintIndex(BlocksIndex, Size);
int RelativeOffset = ListHintIndex ‐ BlocksIndex‐>BaseIndex;

//if there are more of the same size,don't update the bitmap
//如果相同大小的Chunk有很多,就不用更新位图
if(Flink‐>Flink != BlocksIndex‐>ListHead && Flink.Size == Flink‐>Flink.Size)
{
BlocksIndex‐>ListHints[FreeListOffset] = Flink‐>Flink;
}
else
{
BlocksIndex‐>ListHints[FreeListOffset] = NULL;
BlocksIndex‐>ListsInUseUlong[RelativeOffset >> 5] &= ~(1 << RelativeOffset & 0x1F);
}
}

//unlink the current chunk to be allocated
//将当前Chunk从FreeList上取下,以待分配
Blink‐>Flink = Flink;
Flink‐>Blink = Blink;
}

注意:尽管随后会讨论到,我们还是要注意更新bitmap时不会再使用异或(XOR)操作,取而代之的是使用按位与(&)。这防止了1字节FreeListInUseBitmap翻转攻击(John McDonald/Chris Valasek 2009)。

  如果ListHint无法满足内存分配请求,后端堆管理器就会使用Heap->FreeLists。FreeLists包含了堆上所有的空闲Chunks。如果一个足够大的Chunk被找到,那么就会在必要时对它进行拆分并返回给用户。否则,堆就需要使用RtlpExtendHeap()来扩展。

Listing 16. RtlpAllocateHeap Heap->FreeLists allocation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
//attempt to use the FreeLists
//试图使用FreeLists
_LIST_ENTRY *HeapFreeLists = &Heap‐>FreeLists;
_HEAP_LIST_LOOKUP *BlocksIndex = Heap‐>BlocksIndex;
_LIST_ENTRY *ChunkToUse;

//find an appropriate chunk on the FreeLists
//在FreeLists上寻找合适的chunk
_HEAP_LIST_LOOKUP *CurrBlocksIndex = BlocksIndex;

while(1)
{
//if we've ran out of structures abort and we'll extend the heap
//如果搜索完了所有的BlocksIndex结构体还是找不到合适的Chunk,就扩展堆
if(CurrBlocksIndex == NULL)
{
ChunkToUse = CurrBlocksIndex‐>ListHead;
break;
}

//remember that ListHead and HeapFreeLists point to the same location
//记住ListHead和HeapFreeLists指向同一位置
CurrListHead = CurrBlocksIndex‐>ListHead;

//if we've came upon an empty FreeList extend the heap
//如果我们遇到了一个空的FreeList,就扩展heap
if(CurrListHead == CurrListHead‐>Blink)
{
ChunkToUse = CurrListHead;
break;
}

_HEAP_ENTRY *BlinkHeader = (CurrListHead‐>Blink ‐ 8); //FreeLists中最大的Chunk的头指针

//if the chunk is encoded decode it
//如果该Chunk被编码了,就先解码
if(Heap‐>EncodeFlagMask && Heap‐>EncodeFlagMask & BlinkHeader)
{
DecodeHeader(BlinkHeader);
}

//if the chunk can't be serviced by the largest chunk extend the heap
//如果最大的Chunk不能服务,那么就扩展堆
if(SizeInBlocks > BlinkHeader‐>Size)
{
ChunkToUse = CurrListHead;
break;
}

_HEAP_ENTRY *FlinkHeader = CurrListHead‐>Flink‐8; //FreeLists中最小的Chunk的头指针

//if the chunk is encoded decode it
//如果Chunk被编码了,就先解码
if(Heap‐>EncodeFlagMask && Heap‐>EncodeFlagMask & FlinkHeader)
{
DecodeHeader(FlinkHeader);
}

//if the first chunk is sufficient use it otherwise loop through the rest
//如果第一个Chunk的大小是足够的,就使用它,否则继续循环剩余的
if(FlinkHeader‐>Size >= SizeInBlocks)
{
ChunkToUse = CurrListHead‐>Flink;
break;
}
else
{
//loop through all the BlocksIndex‐>ListHints, looking for a sufficiently sized chunk
//then update the bitmap accordingly
//循环所有的BlocksIndex‐>ListHints,寻找一个大小足够的Chunk,然后相应的更新bitmap
}

//look at the next BlocksIndex
//下一个BlocksIndex
CurrBlocksIndex = CurrBlocksIndex‐>ExtendedLookup;
}

Overview(概览)

Back-end allocation

Diagram 4. Back-end allocation

Front-end Allocation(前端分配器)

  现在我们已经看过了LFH是如何通过后端堆管理器的启发式机制来激活的,我们可以看看前端堆管理器使用的分配算法。LFH的设计考虑了性能和可靠性(Marinescu 2006)。为了搞清楚前端分配器的具体工作方式,这些新的增益对逆向工程师来说无疑是巨大的工作量。本节我将尝试阐释一个使用LFH进行分配的典型案例。

RtlpLowFragHeapAllocFromContext

  如前所示,RtlpLowFragHeapAllocFromContext()仅仅在ListHint的Blink的0位被置位时才会被调用。按位操作可以判断出Blink是否包含一个HeapBucket,标志着LFH已做好服务该请求的准备。

  堆管理器的分配一开始需要获取所有的关键数据结构。这包括_HEAP_LOCAL_DATA, _HEAP_LOCAL_SEGMENT_INFO和_HEAP_SUBSEGMENT(可以在图1中看到这些结构的关系)。

  分配器首先会试图使用Hint SubSegment。如果失败则继续尝试使用ActiveSubsegment。如果ActiveSubsegment也失败了,那么分配器必须为LFH设置适当的数据结构以继续(为了避免冗余,下面的代码仅仅展示了Hint Subsegment使用的伪代码,但其逻辑也可以应用于ActiveSubsegment)。

  _INTERLOCK_REQ结构被用来获取当前的Depth, Offset和Sequence。这些信息用来获取一个指向当前空闲Chunk的指针,同时也会计算出下一个可用Chunk的Offset。循环逻辑是为了保证关键数据的更新是原子的,不会在操作期间出现其他修改。

Listing 17. LFH SubSegment allocation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
int LocalDataIndex = 0;

//uses the SizeIndex of the _HEAP_BUCKET to get the address of the LFH for this bucket
//使用_HEAP_BUCKET的SizeIndex来为该bucket获取LFH的地址
_LFH_HEAP *LFH = GetLFHFromBucket(HeapBucket);

//figure this out yourself :)
//这个请自己研究
if(HeapBucket‐>Affinity == 1)
{
AllocateAndUpdateLocalDataIndex();
}

//get the LocalData and LocalSegmentInfo structures based on Affinity and SizeIndex
//根据Affinity和SizeIndex获取LocalData和LocalSegmentInfo结构
_HEAP_LOCAL_DATA *HeapLocalData = LFH‐>LocalData[LocalDataIndex];
_HEAP_LOCAL_SEGMENT_INFO *HeapLocalSegmentInfo = HeapLocalData‐>SegmentInfo[HeapBucket‐>SizeIndex];

//try to use the 'Hint' SubSegment first otherwise this would be 'ActiveSubsegment'
//首先尝试使用'Hint' Subsegment,不成再尝试使用'ActiveSubsegment'
_HEAP_SUBSEGMENT *SubSeg = HeapLocalSegmentInfo‐>Hint;
_HEAP_SUBSEGMENT *SubSeg_Saved = HeapLocalSegmentInfo‐>Hint;

if(SubSeg)
{
while(1)
{
//get the current AggregateExchange information
//获取当前的AggregateExchange信息
_INTERLOCK_SEQ *AggrExchg = SubSeg‐>AggregateExchg;
int Offset = AggrExchg‐>FreeEntryOffset;
int Depth = AggrExchg‐>Depth;
int Sequence = AggrExchg‐>Sequence;

//store the old values, to ensure atomic swapping
//存储旧值,保证原子交换
_INTERLOCK_SEQ AggrExchg_Saved;
AggrExchg_Saved.OffsetAndDepth = AggrExchg.OffsetAndDepth;
AggrExchg_Saved.Sequence = Sequence;

//continue only if this is a valid SubSegment
//仅在是合法的SubSegment情形下才继续
_HEAP_USERDATA_HEADER *UserBlocks = SubSeg‐>UserBlocks;
if(!Depth || !UserBlocks || SubSeg‐>LocalInfo != HeapLocalSegmentInfo)
{
break;
}

//this gets the offset from the AggregateExchg(block size) and creates a byte offset
//从AggregateExchg中获取offset(以blocks为单位),计算出字节偏移
int ByteOffset = Offset * 8;
LFHChunk = UserBlocks + ByteOffset;

//the next offset is store in the 1st 2‐bytes of the userdata (this can probably be abused :))
//下一个offset存储于用户数据的前两个字节中(这可能会被滥用)
short NextOffset = UserBlocks + ByteOffset + sizeof(_HEAP_ENTRY);

//store the updated offset, depth and sequence
//存储更新的offset,depth和sequence
//new_offset = current_offset += BucketSize
//new_depth = current_deth‐‐
//new_sequence = depends on current depth
_INTERLOCK_SEQ AggrExchg_New;
AggrExchg_New.Offset = NextOffset;
AggrExchg_New.Depth = Depth‐‐;
if(AggrExchg_New.Depth == ‐1)
AggrExchg_New.Sequence = Sequence‐‐;
else
AggrExchg_New.Sequence = Sequence;

//i.e InterLockedCompareExchange
if(AtomicSwap(AggrExchg, AggrExchg_New))
{
UpdateHeaders(LFHChunk);
return LFHChunk;
}
else
{
UpdateAffinity();
SubSeg = SubSeg_Saved;
}
}
}

注意:尽管出于格式的原因,我们需要清楚RtlpLowFragHeapAllocFromContext()的所有代码都由try/catch块包裹。这是为了处理LFH中失败发生时,可以返回NULL,此后后端分配器会处理分配请求。

  如果Hint和ActiveSubSegment都失败了,无论是因为未初始化还是无效,RtlpLowFragHeapAllocFromContext()都必须通过分配内存,并且将大块的内存分成HeapBin,来获取一个新的SubSegment(使用后端分配器)。一旦这一步完成了,上面的代码就可以通过ActiveSubsegment来服务请求了。

  如果两种SubSegment都失败了,前端堆就需要分配一个新的内存Chunk。请求的内存量不是任意的,而是基于请求的Chunk的大小以及当前堆上可用的内存总量。下面的伪代码就是我称为Magic Formula(魔法公式)的东西。它将计算需要从后端请求多少内存以便于为一个具体的HeapBucket分割出一个UserBlock:

Listing 18. LFH UserBlocks allocation size algorithm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
int TotalBlocks = HeapLocalSegmentInfo‐>Counters‐>TotalBlocks;

if(MaxRunLenReached)
TotalBlocks = TotalBlocks / 16;

int BucketAffinity = HeapBucket‐>Affinity & 1;

int BucketBytesSize = RtlpBucketBlockSizes[HeapBucket‐>SizeIndex];

int StartIndex = 7;

if(BucketBytesSize < 256)
BucketAffinity‐‐;
if(dword_77F97594 > RtlpHeapMaxAffinity >> 1)
BucketAffinity++;

int BlockMultiplier = 4 ‐ BucketAffinity;

if(TotalBlocks < (1 << BlockMultiplier))
TotalBlocks = 1 << BlockMultiplier;

if(TotalBlocks > 1024)
TotalBlocks = 1024;

//used to calculate cache index and size to allocate
//用于计算要分配的缓存索引和大小
int TotalBlockSize = TotalBlocks * (BucketBytesSize + sizeof(_HEAP_ENTRY)) + 0x18l

if(TotalBlockSize > 0x78000)
TotalBlockSize = 0x78000;

//calculate the cache index upon a cache miss,this index will determine the amount of memory to be allocated
//根据缓存命中计算缓存索引,索引将决定待分配的内存总量
if(TotalBlockSize >= 0x80)
{
do
{
StartIndex++;
}while(TotalBlockSize >> StartIndex);
}

//we will @ most, only allocate 40 pages (0x1000 bytes per page)
//我们至少要分配40个页(0x1000字节/页)
if((unsigned)StartIndex > 0x12)
StartIndex = 0x12;

int UserBlockCacheIndex = StartIndex;

if(HeapBucket‐>Affinity & 6)
UserBlockCacheIndex = 0x12;

//allocate space for a _HEAP_USERDATA_HEADER along with room
//for ((1 << UserBlockCacheIndex)/BucketBytesSize) heap chunks
//为_HEAP_USERDATA_HEADER和((1 << UserBlockCacheIndex)/BucketBytesSize)个堆Chunks分配空间
void *pUserData = RtlpAllocateUserBlock(LFH, UserBlockCacheIndex, BucketByteSize + 8);

_HEAP_USERDATA_HEADER *UserData = (_HEAP_USERDATA_HEADER*)pUserData;

if(!pUserData)
return 0;

  上面的代码中的UserBlockCacheIndex变量用作缓存条目数组的索引值。如果缓存丢失,则使用相同的值计算为UserBlocks Chunk分配多少内存。UserBlocks Chunk随后会被拆分成BucketSize Chunks。让我们看看RtlpAllocateUserBlock在不使用缓存项的情况下是如何封装RtlpAllocateHeap的:

Listing 19. RtlpAllocateUserBlock without caching

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int AllocAmount = 1 << UserBlockCacheIndex;
if(AllocAmount > 0x78000)
AllocAmount = 0x78000;

UserBlock = RtlAllocateHeap(LFH‐>Heap, 0x800000, AllocAmount ‐ 8);
if(UserBlock)
{
LFH‐>CacheAllocs++;

//Assign the _HEAP_USERDATA_HEADER‐>SizeIndex
//赋值_HEAP_USERDATA_HEADER‐>SizeIndex
*(UserBlock+8) = UserBlockCacheIndex;
}

return UserBlock;

  尽管已经分配了内存,但是LFH还没有准备好使用它。它必须首先与_HEAP_SUBSEGMENT耦合。该SubSegment要么是先前被删除的一个,要么创建于_LFH_BLOCK_ZONE链表取回的地址上。

Listing 20. LFH Pre-SubSegment initialization setup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
int UserDataBytesSize = 1 << UserData‐>AvailableBlocks;
if(UserDataBytesSize > 0x78000)
UserDataBytesSize = 0x78000;

int UserDataAllocSize = UserDataBytesSize ‐ 8;

//Increment SegmentCreate to denote a new SubSegment created
//递增SegmentCreate来指示一个新的SubSegment被创建了
InterlockedExchangeAdd(&LFH‐>SegmentCreate, 1);

DeletedSubSegment = ExInterlockedPopEntrySList(HeapLocalData);
_HEAP_SUBSEGMENT *NewSubSegment = NULL;
if(DeletedSubSegment)
{
// if there are any deleted subsegments, use them
// 如果有被删除的subsegments,就使用它
NewSubSegment = (_HEAP_SUBSEGMENT*)(DeletedSubSegment ‐ 0x18);
}
else
{
NewSubSegment = RtlpLowFragHeapAllocateFromZone(LFH, LocalDataIndex);

//return failure use back‐end
//返回失败,使用后端
if(!NewSubsegment)
return 0;
}

//this function will setup the _HEAP_SUBSEGMENT structure
//and check out the data in 'UserData' to be of HeapBucket‐>SizeIndex chunks
//该函数会设置_HEAP_SUBSEGMENT结构体,并检查'UserData'中的数据是否为HeapBucket‐>SizeIndex Chunks
RtlpSubSegmentInitialize(LFH,
NewSubSegment,
UserBlock,
RtlpBucketBlockSizes[HeapBucket‐>SizeIndex],
UserDataAllocSize,
HeapBucket);

//each UserBlock starts with the same sig
//每个UserBlock一开始都有着相同的标记
UserBlock‐>Signature = 0xF0E0D0C0;

  RtlpLowFragHeapAllocateFromZone()具有二重效用:要么为_HEAP_SUBSEGMENT找到一个指针,要么为后续的地址跟踪创建多个_LFH_BLOCK_ZONE结构。

  该函数首先会检查是否存在有效的_LFH_BLOCK_ZONE结构保存了一个SubSegment使用的地址。如果没有或者超出了设计的限制,那么就会分配0x3F8(1016)字节的内存来存储新的_LFH_BLOCK_ZONE对象。下面的代码展示了RtlpLowFragHeapAllocateFromZone()的经典工作情景。

Listing 21. RtlpLowFragHeapAllocateFromZone

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
_LFH_BLOCK_ZONE *CrtZone = LFH‐>LocalData[LocalDataIndex]‐>CrtZone;
_LFH_BLOCK_ZONE *CrtZoneFlink = NULL;

while(1)
{
while(1)
{
while(1)
{
//Flink == NULL => create initial zones
CrtZoneFlink = CrtZone‐>ListEntry‐>Flink;
if(!CrtZone‐>Flink)
break;

void *FreePointer = CrtZoneFlink‐>FreePointer;

//This will increment it to the next SubSegment
//这会递增到下一个SubSegment
void *FreePointer_New = FreePointer + LFH‐>ZoneBlockSize;

//if we've exceeded the limit create more zones
//如果超出了限制,就创建更多的zones
if(FreePointer_New >= CrtZoneFlink‐>Limit)
break;

//InterlockedCompareExchange
//loop if this fails
//如果失败就一直循环,原子操作
if(CompareExchange(&CrtZoneFlink‐>FreePointer,FreePointer_New))
return FreePointer;
}

if(CrtZoneFlink == CrtZone‐>ListEntry.Flink)
break;
}

//this will effectively give us 31 _LFH_BLOCK_ZONE structures to use for keeping track of userdata
//这将有效的给我们31个_LFH_BLOCK_ZONE结构体,用于跟踪用户数据
void *NewLFHBlockZone = RtlAllocateHeap(LFH‐>Heap, 0x80000u, 0x3F8u);
if(!NewLFHBlockZone)
return 0;

//if the CrtZone's ListEntry is empty
//如果CrtZone的ListEntry是空的
if(CrtZoneFlink == CrtZone‐>ListEntry.Flink)
{
//link in the newly created structure into the LFH‐>SubSegmentZones
//把新创建的结构链入到LFH‐>SubSegmentZones
LinkInBlockZone(LFH, NewLFHBlockZone);

//points to the end of the allocations
//指向分配的末尾
NewLFHBlockZone‐>Limit = NewLFHBlockZone + 0x3F8;

//sizeof(_LFH_BLOCK_ZONE) == 0x10
char *AlignedZone = RoundAlign(NewLFHBlockZone + 0x10);

NewLFHBlockZone‐>FreePointer = AlignedZone;
CrtZone‐>ListEntry.Flink = NewLFHBlockZone;
continue;
}
// if we failed, free the data
// 如果失败了,就释放数据
RtlFreeHeap(LFH‐>Heap, 0x800000, NewLFHBlockZone);
}

  尽管上面的代码不太好理解,但它仅考虑了一些设计目的。最内层的循环保证了FreePointers的原子交换,避免了多线程之间的条件竞争。最外层的循环保证了函数在资源耗尽时创建新的BlockZones。

  当通过RtlpLowFragHeapAllocateFromZone()获取到地址时,就可以在RtlpSubSegmentInitialize()中初始化SubSegment。顾名思义,它负责初始化_HEAP_SUBSEGMENT,使用了一大堆参数,比如新创建的SubSegment(NewSubSegment),最近分配的内存(UserBlock),可用内存量(UserDataAllocSize)以及要创建的Chunks的大小(HeapBucket/BucketBytesSize)。

  RtlpSubSegmentInitialize()首先基于HeapBucket大小获取LocalSegmentInfo和LocalData结构。在确保具体的Affinity状态后,它会精准地计算该UserBlock有多少个可用的Chunks。一旦确定要创建的Chunks的数量,他就会迭代内存的大块Chunk,为每个Chunk写入一个头部。最后_INTERLOCK_SEQ的初始值被设置为Depth为NumberOfChunks,而FreeEntryOffset为0x2。

Listing 22. RtlpSubSegmentInitialize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
void *UserBlockData = UserBlock + sizeof(_HEAP_USERDATA_HEADER);

_HEAP_LOCAL_SEGMENT_INFO *LocalSegmentInfo =
LFH‐>LocalData[NewSubSegment‐>AffinityIndex]‐>SegmentInfo[HeapBucket‐>SizeIndex];

_HEAP_LOCAL_DATA *LocalData =
LFH‐>LocalData[NewSubSegment‐>AffinityIndex]‐>Segmentinfo[HeapBucket‐>SizeIndex]‐>LocalData;

if (!((HeapBucket‐>Affinity >> 1) & 3))
{
int TotalBucketByteSize = BucketByteSize + sizeof(_HEAP_ENTRY);
int BucketBlockSize = TotalBucketByteSize / 8;

//sizeof(_HEAP_USERDATA_HEADER) == 0x10
int NumberOfChunks = (UserDataAllocSize ‐ 0x10) / TotalBucketByteSize;

//skip past the header, so we can start chunking
//跳过头部,我们可以开始Chunking
void *pUserData = UserBlock + sizeof(_HEAP_USERDATA_HEADER);

//assign the SubSegment
//赋值给SubSegment
UserBlock‐>SubSegment = NewSubSegment;

//sizeof(_HEAP_USERDATA_HEADER) == 0x10 (2 blocks)
int SegmentOffset = 2;

_INTERLOCK_SEQ AggrExchg_New;
AggrExchg_New.FreeEntryOffset = 2;

if(NumberOfChunks)
{
int NumberOfChunksItor = NumberOfChunks;
do
{
SegmentOffset += BucketBlockSize;
pUserData = UserBlockData;
UserBlockData += BucketByteSize;

//next FreeEntryOffset
*(WORD*)(pUserData + 8) = SegmentOffset;

//Set _HEAP_ENTRY.LFHFlags
*(BYTE*)(pUserData + 6) = 0x0;

//Set _HEAP_ENTRY.UnusedBytes
*(BYTE*)(pUserData + 7) = 0x80;

EncodeDWORD(LFH, pUserData)
}
while(NumberOfChunksItor‐‐);
}

//‐1 indicates last chunk in the UserBlock (_HEAP_USERDATA_HEADER)
//‐1表示UserBlock中的最后一个chunk
*(WORD*)(pUserData + 8) = ‐1;

//Sets all the values for this subsegment
//为该subsegment设置所有的值
InitSubSegment(NewSubSegment);

//updates the bucket counter to reflect NumberOfChunk as total blocks and sets the SubSegmentCount
//更新bucket计数器,以反映NumberOfChunk作为总块数,并设置SubSegmentCount
UpdateBucketCounters(LocalSegmentInfo);

//will be atomically assigned to the NewSubSegment's _INTERLOCK_SEQ
//将原子的对NewSubSegment的_INTERLOCK_SEQ结构赋值
AggrExchg_New.Depth = NumberOfChunks;
AggrExchg_New.Sequence = AggrExchg_Saved.Sequence + 1;

//InterlockedCompareExchange64
AtomicSwap(&NewSubSegment‐>AggregateExchg, AggrExchg_New);
}

  最后,在UserBlocks被分配以后,对SubSegment赋值并初始化,LFH就可以设置ActiveSubsegment为刚刚初始化的那个。它会使用一些锁机制进行操作,最终原子地对ActiveSubsegment赋值。最后执行流将返回到Listing 17的点。

Listing 23. ActiveSubsegment assignment

1
2
3
//now used for LFH allocation for a specific bucket size
//现在为特定bucket大小做LFH分配
AtomicSwap(&HeapLocalSegmentInfo‐>ActiveSegment, NewSubSegment);

Overview(概览)

Front-end allocation

Diagram 5. Front-end allocation

Example(示例)

  想要完整理解分配过程的最佳方法就是通过实例来分析。我们假定LFH已经被激活,并且我们正在处理第一个分配请求,该请求将由前端分配器完成。当收到0x28(40)字节分配请求时,因为头部大小的关系,大小会调整为0x30(48)字节(0x6 blocks)。我们还假定将使用_HEAP_LOCAL_DATA结构中SegmentInfo[0x6]处的ActiveSubSegment。

备注:LFH->LocalData[0]->SegmentInfo[0x6]->ActiveSubsegment->UserBlocks

  根据上面的魔法公式,我们可以推断出对0x30字节来说有0x2A个Chunks(对应Depth)。初始化偏移量为0x2,因为_HEAP_USERDATA_HEADER是0x10字节。

  UserBlock中的每个Chunk都包含一个8字节头部,前4个字节被编码过,调用过程返回的是其后的n字节用户可写的内存。用户可写的前两个字节赋值给了_INTERLOCK_SEQ的NextOffset。

  每个Offset都是从UserBlock Chunk的开头开始计算的,以blocks为单位。下一个可用Chunk的字节Offset将是UserBlocks + FreeEntryOffset * 0x8。

Full UserBlock for Bucket 0x6

Diagram 6. Full UserBlock for Bucket 0x6

  进行初始分配后,Depth和Offset都会进行更新,以反映UserBlock中的下一个可用块。内存实际上并没有移动,只是索引有所不同,下图将展示一次分配后的可用内存状态。注意Offset的值是之前存储在Chunk中的那个(即NextOffset),并且Depth减少0x1; 表示我们已经使用了一个块,并且剩余了0x29。

UserBlock after the 1st allocation for 0x30 bytes

Diagram 7. UserBlock after the 1st allocation for 0x30 bytes

  在第二次分配之后,偏移量UserBlock+0xE将成为下一个空闲块。此后,Userblock+0x14将是下一个空闲块,依此类推。它会不断递增Offset,递减Depth,直到Depth等于0为止。这表示需要为另一个UserBlock分配更多的内存。下图是0x30字节UserBlock两次连续分配之后的状态。我们将在释放(Freeing)一节中看到这些块是如何被释放的。

UserBlock after the 2nd consecutive allocation for 0x30 bytes

Diagram 8. UserBlock after the 2nd consecutive allocation for 0x30 bytes

Freeing(释放)

  现在我们已经对Windows 7的内存分配有了基本的了解,我们可以讨论它是如何释放内存的了。使用中的Chunk会被应用程序释放,并交还给堆管理器。此过程从RtlFreeHeap()开始,它把heap,flags和待释放的chunk作为参数。该函数首先鉴别该chunk是否是可以释放的(free-able),然后检查该Chunk的头部以确定应该由哪个堆管理器负责释放它。

Listing 24. RtlFreeHeap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
ChunkHeader = NULL;

//it will not operate on NULL
//如果为NULL,将无需操作
if(ChunkToFree == NULL)
return;

//ensure the chunk is 8‐byte aligned
//确保要释放的Chunk的地址8字节对齐
if(!(ChunkToFree & 7))
{
//subtract the sizeof(_HEAP_ENTRY)
//剪掉sizeof(_HEAP_ENTRY)
ChunkHeader = ChunkToFree ‐ 0x8;

//use the index to find the size
//使用index来寻找大小
if(ChunkHeader‐>UnusedBytes == 0x5)
ChunkHeader ‐= 0x8 * (BYTE)ChunkToFreeHeader‐>SegmentOffset;
}
else
{
RtlpLogHeapFailure();
return;
}

//position 0x7 in the header denotes whether the chunk was allocated via
//the front‐end or the back‐end (non‐encoded ;) )
//头部的0x7位置指示了chunk是前端还是后端分配的(未编码)
if(ChunkHeader‐>UnusedBytes & 0x80)
RtlpLowFragHeapFree(Heap, ChunkToFree); // 前端释放器
else
RtlpFreeHeap(Heap, Flags | 2, ChunkHeader, ChunkToFree); // 后端释放器

return;

Back-end Freeing(后端释放器)

  后端堆管理器负责处理那些前端堆管理器处理不了的内存,无论是因为大小还是因为LFH的缺失。所有超过0xFE00 blocks的分配都是由VirtualAlloc()/VirtualFree()直接处理,所有超过0x800 blocks的以及那些不能被前端处理的都由后端堆管理器处理。

RtlpFreeHeap

  RtlpFreeHeap()以_HEAP, Flags, ChunkHeader和ChunkToFree作为参数。他将首先试图解码Chunk头部(如果被编码了的话),然后在BlocksIndex内找到一个合适的ListHint。如果无法找到一个足够容纳待释放块的ListHint索引,它将使用BlocksIndex->ArraySize-1作为ListHint 的索引。

Listing 25. RtlpFreeHeap BlocksIndex search

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
if(Heap‐>EncodeFlagMask)
DecodeAndValidateChecksum(ChunkHeader);

int ChunkSize = ChunkHeader‐>Size;
_HEAP_LIST_LOOKUP *BlocksIndex = Heap‐>BlocksIndex;

while(1)
{
//if the chunk will fit in this BlocksIndex, break out
//如果Chunk匹配该BlocksIndex,跳出去
if(ChunkSize < BlocksIndex‐>ArraySize)
break;

//if the chunk is too big for this blocksindex and there is NOT
//and extended lookup, then free onto FreeList[BlocksIndex‐>ArraySize‐1]
//如果chunk太大了,且后面没有扩展了,就释放到FreeList[BlocksIndex‐>ArraySize‐1]
if(!BlocksIndex‐>ExtendedLookup)
{
ChunkSize = BlocksIndex‐>ArraySize ‐ 1;
break;
}

//next item in the linked list
//BlocksIndex链表的下一个条目
BlocksIndex = BlocksIndex‐>ExtendedLookup;
}

注意:搜索ListHint索引并返回的过程从现在开始将视为BlocksIndexSearch()。它将使用_HEAP_LIST_LOOKUP和ChunkSize作为输入。它将遍历链表,更新BlocksIndex参数,直到找到候选者为止,最后返回FreeListIndex。

  现在_HEAP_LIST_LOOKUP已经找到了,该函数可以尝试使用特定的ListHint了。ListHint可以是一个特定的值,比如ListHints[0x6],或者,如果待释放的Chunk的大小大于该BlocksIndex管理的额度,它就会被释放到ListHints[BlocksIndex->ArraySize-BaseIndex-1]。(类似于以前的FreeList[0]链表)

Listing 26. RtlpFreeHeap ListHint retrieval

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//attempt to locate a freelist
//试图定位freelist
_LIST_ENTRY *ListHint = NULL;

//if the chunk can be managed by specific BlocksIndex
//如果Chunk可以由特定BlocksIndex来管理
if(ChunkSize < (BlocksIndex‐>ArraySize ‐ 1) ||
BlocksIndex‐>ExtendedLookup != 0x0 &&
ChunkSize == (BlocksIndex‐>ArraySize ‐ 1))
{
//get the offset into the ListHints
//获取ListHints的Offset
int BaseIndex = BlocksIndex‐>BaseIndex;
int FreeListIndex = RelativeSize(BlocksIndex, ChunkSize ‐ BaseIndex);

//acquire a freelist
//获得一个freelist
ListHint = BlocksIndex‐>ListHints[FreeListIndex];
}

  如果ListHint已经被找到,并且blink不包含HeapBucket,那么后端堆管理器就会更新LFH启发式策略所用的值。由于一个Chunk被放回堆上,它会从计数器中减去0x2。这实际上意味着想要对给定的Bucket激活LFH,至少要进行0x11次连续分配。

  例如,如果Bucket[0x6]收到0x10个请求,此后那些Chunks中的0x2个释放回堆,接着再进行0x2次同样大小的分配,LFH对Bucket[0x6]来说不会启用。在激活启发式方法将执行堆维护之前,必须满足该阈值。

Listing 27. RtlpFreeHeap LFH counter decrement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if(ListHint != NULL)
{
int FreeListBlink = ListHint‐>Blink;

//if the blink is not populated with a HeapBucket
//decrement the counter, as we just freed a chunk
//如果blink没有用HeapBucket填充
//递减计数器,因为我们释放了一个Chunk
if( !(BYTE)FreeListBlink & 1)
{
if(FreeListBlink >= 2)
ListHint‐>Blink = FreeListBlink ‐ 2;
}
}

  在更新了用于LFH激活的计数器后,如果堆允许的话,RtlpFreeHeap()将试图合并Chunk。Chunk合并是一个非常重要的过程,在这个过程中,堆将查看与被释放的Chunk相邻的两个Chunk。这是为了避免有太多的小的空闲Chunks挨在一起(LFH直接解决了这个问题)。尽管RtlpCoalesceFreeBlocks()总是被调用,但Chunk合并仅仅只在被释放的Chunk的相邻Chunk的状态为Free时才会发生。

  一旦相邻块的合并完成,将会继续检查合并后产生的新Chunk的大小,以保证其不超过Heap->DeCommitThreshold,也要确保它不需要由virtual memory来处理。最后,该算法片段将标记Chunk为FREE态,并且使UnusedBytes为0。

Listing 28. RtlpFreeHeap Chunk coalescing and header reassignment

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//unless the heap says otherwise, coalesce the adjacent free blocks
//除非特定情形,堆将合并毗邻的free blocks
int ChunkSize = ChunkHeader‐>Size;

if( !(Heap‐>Flags & 0x80) )
{
//combine the adjacent blocks
//合并毗邻blocks
ChunkHeader = RtlpCoalesceFreeBlocks(Heap, ChunkHeader, &ChunkSize, 0x0);
}

//reassign the ChunkSize if neccessary
//如果有必要,重置ChunkSize
ChunkSize = ChunkHeader‐>Size;

//if the coalesced chunk is bigger than the decommit threshold for this heap, decommit the memory
//如果合并后的Chunk大于该堆的decommit阈值,就decommit该内存
if(ChunkHeader‐>Size > DeCommitThreshold
|| ChunkHeader‐>Size + TotalFreeBlocks > DeCommitThreshold)
{
RtlpDeCommitFreeBlock(Heap, ChunkHeader, ChunkSize, 0x0);
return;
}

//VirtualMemoryThreshold=0xFE00
if(ChunkSize > 0xFE00)
{
RtlpInsertFreeBlock(Heap, ChunkHeader, ChunkSize);
UpdateTagEntry(ChunkHeader);
return;
}

//mark the chunk as FREE
//标记该Chunk为FREE
ChunkToFreeHeader‐>Flags = 0x0;
ChunkToFreeHeader‐>UnusedBytes = 0x0;

  一个空闲Chunk必须被放置在FreeLists上特定的位置,或者至少放在FreeList[0]风格的结构ListHints[ArraySize-BaseIndex-1]中。该过程的第一步就是遍历_HEAP_LIST_LOOKUP来找到一个插入点。此后它会进一步遍历ListHead,如果你还记得的话,它与Heap->FreeLists是相同的指针。该指针以最小的空闲Chunk开始,并向上链接到最大的空闲Chunk。

  循环被用来迭代此堆上可用的所有_HEAP_LIST_LOOKUP结构。该算法会获取ListHead并做一些初始验证。首先检查链表是否为空,如果是的话,循环会终止,执行流继续。其次要确保待释放的Chunk与该链表匹配。它将通过比较链表的最后一项(ListHead->Blink)的大小是否大于待释放Chunk的大小,来实现此目的。

  最后,它会检查ListHead的第一个条目来判断它是否可以在此前插入。如果不行的话,FreeLists将被遍历以找到新的释放的Chunk可以被链入的位置,从FreeListIndex位置开始。(请参考图3中有关FreeLists的信息。)你现在可以看到为什么这些条目被归类为ListHints,这是因为它们实际上并不是那种以指向哨兵节点而终止的专门的链表(旧的FreeLists),相反,它们是指向整个FreeLists中某个位置的指针。

Listing 29. RtlpFreeHeap Insertion point search

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
//FreeList will determine where, if anywhere there is space
//FreeList将确定在哪里有空间
BlocksIndex = Heap‐>BlocksIndex;
_LIST_ENTRY *InsertList = Heap‐>FreeLists;

//attempt to find where to insert this item on the ListHead list for a particular BlocksIndex
//试图在特定BlocksIndex的ListHead链表中找到插入该条目的位置
if(BlocksIndex)
{
//find a BlocksIndex for storage
//查找用于存储的BlocksIndex
int FreeListIndex = BlocksIndexSearch(BlocksIndex, ChunkSize);

while(BlocksIndex != NULL)
{
_HEAP_ENTRY *ListHead = BlocksIndex‐>ListHead;

//if the ListHead is empty,our insert list will be the sentinel node
//如果ListHead为空,我们的插入链表的位置就是哨兵节点
if(ListHead == ListHead‐>Blink)
{
InsertList = ListHead;
break;
}

//if the chunk is larger than the largest entry, we'll insert it after
//如果Chunk大于最大的条目,就插入到最大条目的后面
if(ChunkSize > ListHead‐>Blink.Size)
{
InsertList = ListHead;
break;
}

//pick the insertion point behind the 1st chunk larger than the ChunkToFree
//找到第一个大于ChunkToFree的Chunk位置,选择其为插入点
_LIST_ENTRY *NextChunk = ListHead‐>Flink;
if(NextChunk.Size > ChunkSize)
{
InsertList = NextChunk;
break;
}

NextChunk = BlocksIndex‐>ListHints[FreeListIndex];
while(NextChunk != ListHead)
{
//there is actually some decoding done here
//实际上是一些解码操作
if(NextChunk.Size > ChunkSize)
{
InsertList = NextChunk;
break;
}
NextChunk = NextChunk‐>Flink;
}

//if we've found an insertion spot terminate the loop
//如果找到了插入点,就终止循环
if(InsertList != Heap‐>FreeLists)
break;
BlocksIndex = BlocksIndex‐>ExtendedLookup;
}
}

注意:为了简洁,Chunk头的解码以获取大小的代码没有列出。请不要误以为它是无需解引用的未编码头。

  当插入位置被精准地锁定后,RtlpFreeHeap()将确保Chunk被链入到了合适的位置。一旦找到了Chunk插入的最终位置,就将其安全的链入到FreeList。据我所知,此功能是新增功能,它可以直接解决Brett Moore的插入攻击(Moore 2005)。最后,该Chunk被放置在合适的FreeList上, ListsInUseUlong也相应更新。

Listing 30. Safe link-in

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
while(InsertList != Heap‐>FreeLists)
{
if(InsertList.Size > ChunkSize)
break;
InsertList = InsertList‐>Flink;
}

//R.I.P FreeList Insertion Attack
//R.I.P FreeList插入攻击
if(InsertList‐>Blink‐>Flink == InsertList)
{
ChunkToFree‐>Flink = InsertList;
ChunkToFree‐>Blink = InsertList‐>Blink;
InsertList‐>Blink‐>Flink = ChunkToFree;
InsertList‐>Blink = ChunkToFree;
}
else
{
RtlpLogHeapFailure();
}

BlocksIndex = Heap‐>BlocksIndex;
if(BlocksIndex)
{
FreeListIndex = BlocksIndexSearch(BlocksIndex, ChunkSize);
int RelSize = ChunkSize ‐ BlocksIndex‐>BaseIndex;
FreeListIndex = RelativeSize(BlocksIndex, RelSize);
_LIST_ENTRY *FreeListToUse = BlocksIndex‐>ListHints[FreeListIndex];
if(ChunkSize >= FreeListToUse.Size)
{
BlocksIndex‐>ListHints[FreeListIndex] = ChunkToFree;
}

//bitwise OR instead of the XP XOR (R.I.P Bitmap flipping (hi nico))
//按位或操作取代了XP时代的异或
if(!FreeListToUse)
{
int UlongIndex = Chunkize ‐ BlocksIndex‐>BaseIndex >> 5;
int Shifter = ChunkSize ‐ BlocksIndex‐>BaseIndex & 1F;
BlocksIndex‐>ListsInUseUlong[UlongIndex] |= 1 << Shifter;
}
EncodeHeader(ChunkHeader);
}

注意:注意到ListInUseUlong用了一个按位OR操作,而不是此前所用的XOR操作。这确保了被填充的链表总是被标记为被填充态,而空的链表不可能被标记为填充态。

提示:如果RtlpLogHeapFailure()没有终止执行流将发生什么?(Flink/Blink将永远不会更新。。。)

Overview(概览)

RtlpFreeHeap overview

Diagram 9. RtlpFreeHeap overview

Front-end Freeing(前端释放器)

  前端分配由LFH处理。虽然它没有被首先使用,但是一旦触发了某种启发式操作,它便是唯一使用的堆管理器。设计LFH是为了避免内存碎片并支持频繁的使用特定大小的内存,它与旧的前端管理器Lookaside链表完全不同,Lookaside是通过链表结构来维护小于1024字节的Chunks。尽管BlocksIndex结构可以跟踪大小超过16k的Chunks,但LFH也仅仅为小于16k的Chunks服务。

RtlpLowFragHeapFree

  RtlpLowFragHeapFree()具有两个参数,一个_HEAP结构体和一个指向待释放的Chunk指针。该函数会首先检查在ChunkToFree的头中是否设置了某些flags。如果flags为0x5,则进行调整以更改头部的位置。此后它会找到相关联的SubSegment,SubSegment使得它可以访问内存跟踪时所有需要的成员。它也会重置头部的一些值来反映出其是一个最近释放的块。

Listing 31. RtlpLowFragHeapFree Subsegment acquisition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//hi ben hawkes :)
_HEAP_ENTRY *ChunkHeader = ChunkToFree ‐ sizeof(_HEAP_ENTRY);
if(ChunkHeader‐>UnusedBytes == 0x5)
ChunkHeader ‐= 8 * (BYTE)ChunkHeader‐>SegmentOffset;

_HEAP_ENTRY *ChunkHeader_Saved = ChunkHeader;

//gets the subsegment based off the LFHKey, Heap and ChunkHeader
//根据LFHKey,Heap,ChunkHeader获得Subsegment
_HEAP_SUBSEGMENT SubSegment = GetSubSegment(Heap, ChunkToFree);
_HEAP_USERDATA_HEADER *UserBlocks = SubSegment‐>UserBlocks;

//Set flags to 0x80 for LFH_FREE (offset 0x7)
//设置ExtendedBlockSignature为0x80,表示此由LFH管理的块的状态为Free
ChunkHeader‐>UnusedBytes = 0x80;

//Set SegmentOffset or LFHFlags (offset 0x6)
//设置SegmentOffset或LFHFlags
ChunkHeader‐>SegmentOffset = 0x0;

注意:Ben Hawkes解释了如何使用覆盖Chunk Header的方法来更改ChunkHeader指针,从而导致半控制的内存被释放。你可以选择至多(0x8*0xFF)大小的ChunkHeader内存地址空间。

  一个合适的Chunk Header被定位到以后,函数将需要计算出一个新的偏移。该偏移将用来写入到与SubSegment关联的UserBlock中。此时还需要做一些初始的检查来保证在实际释放Chunk前,Subsegment没有超过它的边界。如果这些情况不满足,就会设置一个值来标识SubSegment需要进一步的维护操作。

  接下来,将尝试从SubSegment获取一个_INTERLOCK_SEQ,获取当前的Depth, Offset和Sequence。下一个Offset将通过该待释放Chunk的前向毗邻的Chunk来获取。正如我们在RtlpLowFragHeapAllocFromContext()中所看到的,它被存储在Chunks数据域的前2个字节处。Depth会递增1,这是因为一个Chunk刚刚被放回可用bin中。

  新值与旧值将进行原子交换,成功则跳出循环,失败则循环继续。这是LFH中大多数的典型操作,它被设计成用于高并发环境下工作。

Listing 32. RtlpLowFragHeapFree OffsetAndDepth/Sequence update

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
while(1)
{
_INTERLOCK_SEQ AggrExchg;
AggrExchg.OffsetAndDepth = SubSegment‐>AggregateExchg.OffsetAndDepth;
AggrExchg.Sequence = SubSegment‐>AggregateExchg.Sequence;

int Sequence_New = AggrExchg.Sequence + 1;
if(AggrExchg.OffsetAndDepth >= ‐1)
Sequence_New‐‐;

int Depth = SubSegment‐>AggrExchg.Depth;
_HEAP_LOCAL_SEGMENT_INFO *LocalSegmentInfo = SubSegment‐>LocalInfo;
unsigned int Sequence = SubSegment‐>LocalInfo‐>LocalData‐>Sequence;
unsigned int LastOpSequence = LocalSegmentInfo‐>LastOpSequence;

//setup the new INTERLOCK_SEQ
//设置新的INTERLOCK_SEQ
_INTERLOCK_SEQ AggrExchg_New;
AggrExchg_New.Sequence = Sequence_New;

if(Depth != SubSegment‐>BlockCount
|| LocalSegmentInfo‐>Counters.SubSegmentCounts == 1)
{
if(Sequence >= LastOpSequence && (Sequence ‐ LastOpSequence) < 0x20)
{
//set the FreeEntryOffset of ChunkToFree
//为ChunkToFree设置FreeEntryOffset
*(WORD)(ChunkHeader + 8) = AggrExchg.FreeEntryOffset;

//Subtract the size of the block being freed from the current offset;
//which will give you the next free chunk
//从当前Offset扣除释放的block大小;得到下一个free chunk的Offset
int NewOffset = AggrExchg.FreeEntryOffset ‐ (ChunkHeader ‐ UserBlocks) / 8;
AggrExchg_New.FreeEntryOffset = NewOffset;

//increase depth because we're freeing
//递增Depth,因为我们释放了Chunk
AggrExchg_New.Depth = Depth + 1;

//set the Hint in the subsegment
//在Subsegment中设置Hint
Sequence = 1;
SubSegment‐>LocalInfo‐>Hint = SubSegment;
}
else
{
Sequence = 3;
AggrExchg_New.Depth = ‐1; //last entry
AggrExchg_New.FreeEntryOffset = 0; //no offset
}
}
else
{
Sequence = 3;
AggrExchg_New.Depth = ‐1; //last entry
AggrExchg_New.FreeEntryOffset = 0; //no offset
}

//_InterlockedCompareExchange64
if(AtomicSwap(&SubSegment‐>AggregateExchg, AggrExchg_New, AggrExchg))
break;

//if something has changed since swapping, try again
//交换时如果发生了什么变化,就重试
ChunkHeader = ChunkHeader_Saved;
}

注意:你可以看到Subsegment->Hint是在这里赋值的,它将用于后续的分配。

  最后,将检查Sequence变量是否被设置成了0x3。如果是的话,就说明SubSegment需要执行更多操作,UserBlocks Chunk可以被释放(通过后端堆管理器);如果不是的话,就会返回0x1。

Listing 33. RtlpLowFragHeapFree Epilog

1
2
3
4
5
6
7
8
9
10
11
12
13
//if there are cached items handle them
//如果有缓存条目,就处理它们
UpdateCache(SubSegment);

//if we've freed every item in the list update the subsegment and free the UserBlock
//如果我们释放了list中每一个条目,就更新Subsegment并释放UserBlock
if(Sequence == 3)
{
PerformSubSegmentMaintenance(SubSegment);
RtlpFreeUserBlock(LFH, SubSegment‐>UserBlocks);
}

return 1;

注意:PerformSubSegmentMaintenance()不是一个真正的函数,只是一系列复杂指令的别名,它将为SubSegment做好释放或后续使用的准备。

Overview(概览)

RtlpLowFragHeapFree overview

Diagram 10. RtlpLowFragHeapFree overview

Example(示例)

  继续看本文分配一节中的例子,我们将进行第三次的0x30字节的连续分配。这意味着还有0x27个Chunks剩余(每个0x30大小),并且到下一个块的当前偏移量是UserBlock的0x14; 它看起来像这样:

UserBlock after the 3rd consecutive allocation for 0x30 bytes

Diagram 11. UserBlock after the 3rd consecutive allocation for 0x30 bytes

  当我们释放从LFH分配的内存时会发生什么呢?如前所述,该内存实际上并没有移动到任何地方,只是Offset被更新,它用作UserBlocks中下一个空闲Chunk位置的索引。

  现在假设从UserBlocks分配的第一个Chunk被释放了,此时需要更新第一个Chunk的Offset,并将Depth递增0x1。通过获取Chunk Header的地址,减去UserBlock的地址,然后将结果除以0x8,可以计算出新的Offset。也就是说,新的Offset来源于UserBlock的相对位置(以blocks为单位)。下面的图展示了UserBlock在第一个Chunk被释放时的状态。

Freeing of the 1st chunk allocated for 0x30 bytes

Diagram 12. Freeing of the 1st chunk allocated for 0x30 bytes

  现在想象一下,已分配的第二个Chunk也被释放了(在其他任何分配或释放之前)。新的Offset就会变成0x8,这是因为第二个Chunk的释放在第一个Chunk之后。尽管在Offset 0x2处有空闲Chunk,但下一个被用来分配的Chunk位置位于Offset 0x8。这可以想象成是一个链表,它更新它的指针而不是其地址。

Freeing of the 2nd chunk allocated for 0x30 bytes

Diagram 13. Freeing of the 2nd chunk allocated for 0x30 bytes

Security Mechanisms(安全机制)

  大部分在Windows XP SP2中引入的安全机制在Windows 7中并未发生变化,而Windows 7中还引入了一些其他的的安全机制(基于Windows Vista代码)。本节我们将讨论其中的一些安全措施,包括它们是如何实现的,以及对于每个机制的一些想法。话虽如此,我认为在Windows Vista代码基础上引入的所有保护逻辑对Windows堆的漏洞利用产生了迄今为止最大的障碍。

Heap Randomization(堆随机化)

  堆随机化的目的在于使HeapBase拥有一个不可预测的地址。每次创建堆时,都会在基地址上增加一个随机的偏移以防止可预测的内存地址。

  这在RtlCreateHeap()中通过创建一个随机的64k对齐的值并增加到HeapBase上实现。每次执行应用程序时,此值将尝试产生一个变化的地址。随机化可能取决于Heap大小的最大值,该值由传递给HeapCreate()的参数计算出来。下面的代码片段源于RtlCreateHeap(),它试图随机化HeapBase:

Listing 34. RtlCreateHeap randomization

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int BaseAddress = Zero;
int RandPad = Zero;

//get page aligned size to use as a random pad
//获取页对齐大小作为随机数pad
int RandPad = (RtlpHeapGenerateRandomValue64() & 0x1F) << 0x10;

//if maxsize + pad wraps, null out the randpad
//如果maxsize + pad溢出了,就零化randpad
int TotalMaxSize = MaximumSize + RandPad;
if(TotalMaxSize < MaximumSize)
{
TotalMaxSize = MaximumSize;
RandPad = Zero;
}

//0x2000 = MEM_RESERVE
//0x40 = PAGE_EXECUTE_READWRITE
//0x04 = PAGE_READWRITE
//this will reserve the memory at the baseaddress but NOT actually commit any memory at this point
//这将保留位于基址的内存,但此时不会实际提交任何内存
int Opts = 0x4;
if(Options & 0x40000)
Opts = 0x40;

if(NtAllocateVirtualmemory(‐1, &BaseAddress, 0x0, &TotalMaxSize, 0x2000, Opts)
return 0;

Heap = (_HEAP*)BaseAddress;

//adjust the heap pointer by randpad if possible
//如果可能的话,用randpad来调整堆指针
if(RandPad != Zero)
{
if(RtlpSecMemFreeVirtualMemory(‐1, &BaseAddress, &RandPad, 0x8000) >= 0 )
{
Heap = (_HEAP*)RandPad + BaseAddress;
MaximumSize = TotalSize ‐ RandPad;
}
}

Comments(注解)

  对堆来说当使用随机化时,实际上基地址的数量是有限的,这是因为它们需要64k对齐(5bits熵)。通过熵的缺陷来猜测堆基地址可能不切实际,但也不是完全没有可能。

  另一个不太可能的情景源于这样一个事实:如果RandPad+MaximumSize越界,那么RandPad将为NULL。这将有效的使堆随机化无效。我之所以说它不太可能发生是基于两个原因:首先它无法控制HeapCreate()的传参。我知道在应用程序中这可以发生,但它并不通用。其次获取一个足够大的MaximumSize堆往往会引起NtAllocateVirtualMemory()返回NULL,总之是完全失败的。

Header Encoding/Decoding(堆头编码/解码)

  在Windows Vista之前,判断Chunk没有被损坏的唯一方法就是校验Chunk头部的1字节cookie。这显然不是鲁棒性最好的解决方案,因为这个cookie可以被暴力猜解出来,更为重要的是,在这之前有着头部数据(McDonald/Valasek 2009)。

  于是编码Chunk头部的措施应运而生。现在,堆将对每个_HEAP_ENTRY的前4字节进行编码。这将阻止对Size, Flags和Checksum溢出产生的影响。通过异或Chunk Header的前3个字节并存储到SmallTagIndex变量中来完成编码,此后Chunk Header的前4个字节会与Heap->Encoding进行异或(由RtlCreateHeap()随机产生)。

Listing 35. Heap header encoding

1
2
3
4
5
6
7
8
9
EncodeHeader(_HEAP_ENTRY *Header, _HEAP *Heap)
{
if(Heap‐>EncodeFlagMask)
{
Header‐>SmallTagIndex =
(BYTE)Header ^ (Byte)Header+1 ^ (Byte)Header+2;
(DWORD)Header ^= Heap‐>Encoding;
}
}

  对Chunk进行解码和编码很像,但是在实际完成解码前需要进行一些额外的检查。需要确保Chunk的头部被编码过。

Listing 36. Heap header decoding

1
2
3
4
5
6
7
DecodeHeader(_HEAP_ENTRY *Header, _HEAP *Heap)
{
if(Heap‐>EncodeFlagMask && (Header & Heap‐>EncodeFlagMask))
{
(DWORD)Header ^= Heap‐>Encoding;
}
}

Comments(注解)

  对Chunk头部起始4字节的编码,使得覆写Size, Flags或是Checksum字段的操作在不使用信息泄露(info leak)的情况下几乎不可行(Hawkes 2008)。但这也没有阻止我们覆写Chunk头部的其他信息:如果Chunk头部可以被覆写且在值校验之前被使用,那么它就有可能改变执行流。我们会在后续章节中讨论。

  另一个可行的回避方案是去覆写堆管理器,使其认为Chunk是没有被编码的。有多种方法可以做到。第一个也是可能性最低的方法就是NULL化Heap->EncodeFlagMask(被初始化为0x100000)。后续的任何编解码操作都不会进行。这种方法有些缺陷,因为随之而来的是显著的堆不稳定性。一般会创建一个新的堆来达到这种效果(_HEAP_ENTRY头的未编码覆盖)。

  第二种也是最有可能的方法是通过覆盖Chunk头的前4字节,使得其与Heap->EncodeFlagMask的AND位操作可以返回false。这种方法可对Size, Flags和Checksum进行有限的控制。这仅对覆盖FreeLists中头部有用,因为校验和验证是在分配过程中完成的。

  最后,攻击者可以将Chunk头的后4个字节作为目标。我们此前已经看过了,这些字段用于判断Chunk的状态。例如,Chunk头0x7偏移字节用来判断Chunk是来自LFH(前端)还是后端。

Death of bitmap flipping(位图翻转的死亡)

  在Brett Moore的“Heaps about Heaps”一文中,曾对如何欺骗FreeList使其误判自身的填充状态的手法进行了介绍。从那开始,这种手法一度被称为位图翻转(Moore 2008)。这种攻击手法在较新的Windows版本中被直接解决了,因为在2009年McDonald和Valasek输出了一篇质量相当高的论文。(注:这是100%不正确的)

  在Windows XP代码基础上,XOR操作用来更新bitmap。如果逻辑上可以触发该更新操作且此时FreeList为空的话,在bitmap中当前的位会对自身进行XOR操作,这就会使得它的值翻转。

Listing 37. Death of Bitmap Flipping

1
2
3
4
5
6
7
8
9
10
11
// if we unlinked from a dedicated free list and emptied it,clear the bitmap
// 如果我们对专门的freelist摘除并清空,就会清除bitmap
if (reqsize < 0x80 && nextchunk == prevchunk)
{
size = SIZE(chunk);
BitMask = 1 << (size & 7);

// note that this is an xor
// 注意到这是个xor
FreeListsInUseBitmap[size >> 3] ^= vBitMask;
}

  这种技术的问题在于一旦Chunk的Size被损坏了,你就可以在不应该更改专用FreeList的情况下更改其状态;这就会导致我们可以进一步去覆盖HeapBase中的关键数据。

  当更新bitmap来显示一个空链表时,按位与AND操作被使用到,这确保了空的链表可以保持空的状态,而被填充过的链表仅仅可以被标记为空。对于标记一个链表为填充态来说也是一样的,它使用按位或OR操作来改变ListsInUserUlong。如此,一个空的链表可以被标志为填充态,但已填充的链表却不能变成未填充态。

  以上的这些修改,专用FreeLists的概念已经消失了,所以试图从空的链表中分配仅仅是遍历FreeList结构来找到一个大小充足的Chunk。

Listing 38. Death of Bitmap Flipping 2

1
2
3
4
5
6
7
8
9
//HeapAlloc
size = SIZE(chunk);
BitMask = 1 << (Size & 0x1F);
BlocksIndex‐>ListInUseUlong[Size >> 5] &= ~BitMask;

//HeapFree
size = SIZE(chunk);
BitMask = 1 << (Size & 0x1F);
BlocksIndex‐>ListInUseUlong[Size >> 5] |= BitMask;

Safe Linking(安全链入)

  Safe Unlinking机制最早在Windows XP SP2中提出,它可以防止从FreeList上取下Chunk(或者合并两个空闲Chunk)时4字节覆写的行为。从这开始,大量通用的堆的利用被阻止。

  尽管从链表中取下一个条目不能再进行任意地址覆写,但仍然可以覆写FreeList[0]上某个条目的blink去指向你想覆写的地址(Moore 2005)。如此,一旦一个Chunk被插入到了被修改的条目之前,blink就会指向刚刚被释放的Chunk的地址。

  在后端堆管理器中新的检查机制在链入一个空闲Chunk前校验了Chunk的blink。我们在解释RtlpFreeHeap()工作机制时看到了这部分代码,让我们来回顾一下。你可以看到如果FreeList的Blink->Flink不是指向自身,那就认为它已经被破坏而不会再执行链入操作。

Listing 39. Safe Linking

1
2
3
4
5
6
7
8
9
10
11
if(InsertList‐>Blink‐>Flink == InsertList)
{
ChunkToFree‐>Flink = InsertList;
ChunkToFree‐>Blink = InsertList‐>Blink;
InsertList‐>Blink‐>Flink = ChunkToFree;
InsertList‐>Blink = ChunkToFree;
}
else
{
RtlpLogHeapFailure();
}

Comments(注解)

  虽然该设备通过损坏的Blink指针阻止了指针覆写操作,但它仍然存在着一个问题,那就是在RtlpLogHeapFailure()之后进程并没有终止。后面的代码直接把Chunk插入到合适的ListHints槽,而实际上并没有更新flink和blink。这意味着flink和blink是用户完全可控的(译者注:因为flink和blink在data域,释放之后用作flink和blink不会被零化,所以在不改写的情况下释放前是什么释放后还是什么)。

Tactics(利用策略)

Heap Determinism(堆确定性)

  这些年研究者把精力集中在堆的元数据之上,以达成执行流改写。这一琐碎的任务越来越困难了。通用的4字节写攻击已经灭亡,堆头现在也用伪随机数进行了编码,保护了它的完整性,现在即使是老如欺骗FreeList插入攻击的技巧也基本失效了。

  现在,比以往任何时候,漏洞利用程序在尝试设置有利条件时都必须具有很高的精确度。我们在讨论堆时,称之为堆的精心操纵(heap manipulation)。Chunk大小、分配或是释放操作的命令在现代的Windows堆利用中确实发挥了重要作用。

  在本节中,我将尝试讨论在尝试使用LFH使堆处于确定性状态时发生的一些常见场景。比如,哪个位置会分配Chunk X?与分配的Chunk X毗邻的是什么?如果Chunk X被释放会发生什么?

Activating the LFH(激活LFH)

  理解对特定Bucket如何激活LFH是最为基础的信息之一。LFH对Windows 7来说是唯一的前端堆管理器,但这并不意味着它会默认处理所有的特定大小Chunk的分配请求。如上面代码所展示,LFH必须由后端堆管理器的启发式机制激活。如果您可以强制进行分配(这也是相当常见的),那么你就可以为特定大小的Bucket激活LFH。LFH可以由至少0x12次连续分配相同大小Chunk的操作来激活(或者0x11,如果LFH此前已被激活)。

Listing 40. Enable the LFH for a specific size

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//0x10 => Heap‐>CompatibilityFlags |= 0x20000000;
//0x11 => RtlpPerformHeapMaintenance(Heap);
//0x11 => FreeList‐>Blink = LFHContext + 1;
for(i = 0; i < 0x12; i++)
{
printf("Allocation 0x%02x for 0x%02x bytes\n", i, SIZE);
allocb[i] = HeapAlloc(pHeap, 0x0, SIZE);
}

//now that the _HEAP_BUCKET is in the ListHint‐>Blink, the LFH will be used
//现在_HEAP_BUCKET在ListHint‐>Blink中,LFH会被使用
printf("Allocation 0x%02x for 0x%02x bytes\n", i++, SIZE);
printf("\tFirst serviced by the LFH\n");
allocb[i] = HeapAlloc(pHeap, 0x0, SIZE);

  如你所见,为特定大小激活和启用LFH相当简单,如果你有能力控制分配的话,诸如分配DOM对象。现在前端堆管理器是为特定的大小而使用,可以步步为营达成更高层次的确定性。

Defragmentation(碎片整理)

  在我们讨论在LFH上布置相邻的Chunks之前,必需要先讨论碎片化。因为频繁的分配和释放操作,UserBlock Chunk会碎片化。这意味着必须要进行碎片整理操作以保证我们溢出的Chunk与我们想要覆写的Chunk毗邻。

  在下一个例子中我们将把Chunks直接的相邻布局作为前提。尽管处理新的SubSegment是相当简单的,因为当前没有任何坑洞,但是使用已使用的SubSegment则不会有这个问题。下图展示了单次分配不会导致3个毗邻的对象,为此必须要先占坑。最简单的方法就是对当前应用程序的内存布局有一些了解然后做出一些分配动作。

Defragmentation1

  在这种情况下,我们仅需要进行3次分配就可以填充这些坑洞,但是很显然这不是个现实的例子。攻击者往往不知道具体有多少坑洞需要去填充,也不知道需要多少个分配才能完全耗尽UserBlocks(Depth==0x0)。于是就强制堆管理器来创建一个新的SubSegment,他不会包含任何坑洞(说明:感谢Alex/Matt)。

Defragmentation2

Adjacent Data(相邻的数据)

  当试图利用基于堆缓冲区溢出时,最难的任务之一就是精心操纵堆,使得堆的状态已知。你要确保溢出的Chunk与想要被覆写的Chunk是直接毗邻的。通常对后端堆来说,释放内存时的合并操作非常棘手,它会导致exp的不可靠。

注意:在Windows XP/2003 Heap Exploitation这个demo中,试图利用堆缓冲区溢出漏洞时,这可谓是最难的任务。应用程序的多线程特性会不可靠的合并Chunks,让我们对堆的精心操纵失效。

  LFH不会合并Chunks,因为它们的大小全部一致。因此,相对于它们与UserBlock的偏移量进行索引。这使得相同大小的Chunks可以挨着放置非常简单。如果可以溢出,BUSY和FREE态Chunks都可以被覆写,这依赖于UserBlocks当前的状态。

  假设我们想要覆写alloc3的信息,并且处于write-n的情景。只要在alloc3之前有可能溢出的分配,就可以覆盖alloc3中的数据。

Listing 42. LFH Chunk overflow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
EnableLFH(SIZE);
NormalizeLFH(SIZE);

alloc1 = HeapAlloc(pHeap, 0x0, SIZE);

alloc2 = HeapAlloc(pHeap, 0x0, SIZE);
memset(alloc2, 0x42, SIZE);
*(alloc2 + SIZE‐1) = '\0';

alloc3 = HeapAlloc(pHeap, 0x0, SIZE);
memset(alloc3, 0x43, SIZE);
*(alloc3 + SIZE‐1) = '\0';

printf("alloc2 => %s\n", alloc2);
printf("alloc3 => %s\n", alloc3);

memset(alloc1, 0x41, SIZE * 3);

printf("Post overflow..\n");
printf("alloc2 => %s\n", alloc2);
printf("alloc3 => %s\n", alloc3);

Listing 43. LFH Chunk overflow result

1
2
3
4
5
6
7
8
Result:
alloc2 => BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
alloc3 => CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
Post overflow..
alloc2 => AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACCCCCC
CCCCCCCCC
alloc3 => AAAAAAAAAAAAAAAAAAAAAAAACCCCCCCCCCCCCCC

  如你所见,alloc3中的数据被覆盖成了溢出的alloc1数据。另一个值得注意的影响在于溢出发生后alloc2字符串的长度。该字符串的长度实际上是alloc2和alloc3组合在一起的长度,因为null终止符被覆盖掉了。可以阅读Peter Vreugdenhil的论文(Vreugdenhil 2010)来对一个真实的覆盖null终止符案例一探究竟,该利用最后达成了代码执行。

  但是如果alloc2被使用到了或者在alloc3使用前对其头部(alloc2)进行了验证该如何?因此你需要找到那个恰好在被溢出Chunk(alloc3)正前方的Chunk(alloc2)。尽管这可能看起来很简单,但在控制分配和释放时还需要考虑碎片问题。

Listing 44. Chunk reuse

1
2
3
4
5
6
7
8
9
10
11
alloc1 = HeapAlloc(pHeap, 0x0, SIZE);
alloc2 = HeapAlloc(pHeap, 0x0, SIZE);
alloc3 = HeapAlloc(pHeap, 0x0, SIZE);

HeapFree(pHeap, 0x0, alloc2);

//overflow‐able chunk just like alloc1 could reside in same position as alloc2
//像alloc1那样的可溢出块同样可以驻留在与alloc2相同的位置上
alloc4 = HeapAlloc(pHeap, 0x0, SIZE);

memcpy(alloc4, src, user_controlled_size);

注意:尽管精心操纵堆使得Chunk按序相邻在LFH中更简单一些,但却有个重大缺陷。想要布置两个不同大小的Chunks变得更为复杂(涉及了相邻内存上的多个SubSegments)。如果想要控制不同大小的Chunk来达成漏洞利用,那么这无疑是最大的绊脚石。

Seeding Data(播种数据)

  撰写此文之际,UAF漏洞非常流行。这些漏洞的大多数漏洞利用都包含了各种各样的分配方法(JavaScript strings, DOM对象实例等等),以尝试在堆中播种数据。由于缺乏对对象数据的理解,Nico Waisman称这种技术为pray-after-free(Waisman 2010)。

  我们已经知道LFH如何将内存存储在大的UserBlock中,这些UserBlock分为BucketSize块。我们也知道了UserBlock中这些Chunks是可以相互毗邻的,这依赖于分配和释放行为的控制。基于此,我们可以通过将数据写入用户可写的内存来控制每个Chunk的内容(这因HEAP_ZERO_MEMORY标志是否设置这一情况而异,该标志的设置是在对HeapAlloc()的调用中)。

  下面的实例展示了内存如何被拷贝到LFH中的Chunks上,随后进行释放,然后再分配时又不会丢失很多原始数据。

Listing 45. Data seeding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
EnableLFH(SIZE);
NormalizeLFH(SIZE);

for(i = 0; i < 0x10; i++)
{
//分配堆块并初始化
printf("Allocation 0x%02x for 0x%02x bytes => ", i, SIZE);
allocb[i] = HeapAlloc(pHeap, 0x0, SIZE);
memset(allocb[i], 0x41 + i, SIZE);
//打印内容
for(j=0; j<12; j++)
printf("%.2X", allocb[i][j]);
printf("\n");
}

//释放所有堆块
printf("Freeing all chunks!\n");
for(i = 0; i < 0x10; i++)
{
HeapFree(pHeap, 0x0, allocb[i]);
}

//再次分配
printf("Allocating again\n");
for(i = 0; i < 0x10; i++)
{
//再次分配,但不初始化
printf("Allocation 0x%02x for 0x%02x bytes => ", i, SIZE);
allocb[i] = HeapAlloc(pHeap, 0x0, SIZE);
//打印内容
for(j=0; j<12; j++)
printf("%.2X", allocb[i][j]);
printf("\n");
}

Listing 46. Data seeding results

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Result:
Allocation 0x00 for 0x28 bytes => 41414141 41414141 41414141
Allocation 0x01 for 0x28 bytes => 42424242 42424242 42424242
Allocation 0x02 for 0x28 bytes => 43434343 43434343 43434343
Allocation 0x03 for 0x28 bytes => 44444444 44444444 44444444
Allocation 0x04 for 0x28 bytes => 45454545 45454545 45454545
Allocation 0x05 for 0x28 bytes => 46464646 46464646 46464646
Allocation 0x06 for 0x28 bytes => 47474747 47474747 47474747
Allocation 0x07 for 0x28 bytes => 48484848 48484848 48484848
Allocation 0x08 for 0x28 bytes => 49494949 49494949 49494949
Allocation 0x09 for 0x28 bytes => 4A4A4A4A 4A4A4A4A 4A4A4A4A
Allocation 0x0a for 0x28 bytes => 4B4B4B4B 4B4B4B4B 4B4B4B4B
Allocation 0x0b for 0x28 bytes => 4C4C4C4C 4C4C4C4C 4C4C4C4C
Allocation 0x0c for 0x28 bytes => 4D4D4D4D 4D4D4D4D 4D4D4D4D
Allocation 0x0d for 0x28 bytes => 4E4E4E4E 4E4E4E4E 4E4E4E4E
Allocation 0x0e for 0x28 bytes => 4F4F4F4F 4F4F4F4F 4F4F4F4F
Allocation 0x0f for 0x28 bytes => 50505050 50505050 50505050

Freeing all chunks!
Allocating again

Allocation 0x00 for 0x28 bytes => 56005050 50505050 50505050
Allocation 0x01 for 0x28 bytes => 50004F4F 4F4F4F4F 4F4F4F4F
Allocation 0x02 for 0x28 bytes => 4A004E4E 4E4E4E4E 4E4E4E4E
Allocation 0x03 for 0x28 bytes => 44004D4D 4D4D4D4D 4D4D4D4D
Allocation 0x04 for 0x28 bytes => 3E004C4C 4C4C4C4C 4C4C4C4C
Allocation 0x05 for 0x28 bytes => 38004B4B 4B4B4B4B 4B4B4B4B
Allocation 0x06 for 0x28 bytes => 32004A4A 4A4A4A4A 4A4A4A4A
Allocation 0x07 for 0x28 bytes => 2C004949 49494949 49494949
Allocation 0x08 for 0x28 bytes => 26004848 48484848 48484848
Allocation 0x09 for 0x28 bytes => 20004747 47474747 47474747
Allocation 0x0a for 0x28 bytes => 1A004646 46464646 46464646
Allocation 0x0b for 0x28 bytes => 14004545 45454545 45454545
Allocation 0x0c for 0x28 bytes => 0E004444 44444444 44444444
Allocation 0x0d for 0x28 bytes => 08004343 43434343 43434343
Allocation 0x0e for 0x28 bytes => 02004242 42424242 42424242
Allocation 0x0f for 0x28 bytes => 62004141 41414141 41414141

注意:如果一个HeapBin中所有的Chunks都被释放了,那么整个Bin自身也会被释放。这意味着它不可能像Lookaside链表那样具有完全的活塞效应。Jay-Z建议在进行多此释放之前,先分配大量的Chunks,最小化HeapBin因太小而会被释放这一情形的概率。

  首次分配打印出了向LFH每个Chunk写入的内存数据,大小是0x28(增加_HEAP_ENTRY头大小后是0x30)。所有的Chunks都被释放掉,然后以与步骤1中相同的分配方式进行分配。尽管这一次我们没有看到对memset的调用,但是Chunks中的数据却是惊人的相似。

  这是因为这些Chunks既没有被合并也没有被清除数据。被改变的仅仅只有数据域的前两个字节。这就是在算法(Algorithms)一节中谈到的保存的FreeEntryOffset。FreeEntryOffset被保存在Chunk Header之后的内存的前两个字节中,以供堆管理器将来使用。

  还应当注意重新分配的Chunks相对于一开始在UserBlock中原本的顺序来说是颠倒的。这不是因为内存自身做了翻转,而是因为FreeEntryOffset在每次HeapFree()调用时都会重新建立索引。因此在知晓分配大小和旧数据的情况下我们可以做什么呢?对于已知大小分配能力的利用,UAF是一个非常完美的候选者。

  让我们假定某个对象的大小为0x30字节,并且在0x0偏移处有个虚表。该对象在被释放,并进行垃圾回收后,然后被错误的使用到。这就给了我们在释放后再次使用之前可以覆写其数据域的机会,攻击者就可以控制(或半控制)虚表所在的地址。

  由于大小是已知的,通过控制在此释放的对象被使用之前进行一次分配操作,这就给了我们完全的控制权去决定对象的虚表应该使用哪个地址。控制该地址可以让我们有能力改变程序执行流到我们提供的地址上,而这里往往是我们的payload。

Listing 47. Use-after-free contrived example (Sotoriv 2007)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//LFH is enabled
//LFH已经被启用

//0x30 byte object
var obj = new FakeObj();

//.
//.
//.
//obj goes out of scope & garbage collected
//.
//.
//.

var heap = new heapLib.ie();

//this will allocate the same location in memeory as obj
//这将在内存中分配到与obj相同位置的堆块
heap.alloc("A" * 0x30, "overwrite");

//vtable == 0x41414141
//假的虚表指针
obj.DoStuff();

注意:尽管这是一个过于简单的例子,但它仍然展示了使用堆管理如何产生精准的漏洞利用的知识。如果你在Blackhat USA 2010前阅读本文,我强烈建议你参加Nico Waisman的演讲,Aleatory Persistent Threat。如果没有的话请花些时间阅读他的同名的论文,它会提供本文讨论的主题的应用实例。

Exploitation(漏洞利用)

  自从堆利用变得流行之后,若干保护机制相继诞生,从Safe-Unlinking到编码Chunk头,堆利用的难度继续增加。当下的元数据损坏通常用来覆写部署在堆上的应用程序数据,而不是直接达成代码执行。话虽如此,但这不应被认为是不可能的;尽管很困难,元数据损坏仍可用于获得代码执行。

  在本节中我会将堆元数据利用相关的新旧技术一起讨论。尽管这种元数据损坏不能获得简单的write-4,但它会展示在满足一定的前提条件下,如何通过操纵数据来达成代码执行。

Ben Hawkes #1

  Ben Hawkes的RuxCon 2008论文(Hawkes 2008)不仅仅对Windows Vista内存管理给出了一个概览,还对通过堆损坏达成代码执行的新技术进行了阐述。我建议读者在阅读本论文、尤其是本节内容前,优先阅读他的文章。他引入了若干技巧,印象最深的就是他的Heap HANDLE payload,但在本文中我只讨论一种技术。

  Dr.Hawkes还特别的谈到了由LFH管理的UserBlock中的Chunks损坏。当在RtlpLowFragHeapFree()中释放Chunk时,它会检查_HEAP_ENTRY的UnusedBytes(offset 0x7)的值是否是0x5(感觉这里应该是ExtendedBlockSignature)。如果是的话函数会使用SegmentOffset (offset 0x6)作为一个不同的Chunk头部的索引。

Listing 48. Chunk header relocation

1
2
3
_HEAP_ENTRY *ChunkHeader = ChunkToFree ‐ sizeof(_HEAP_ENTRY);
if(ChunkHeader‐>UnusedBytes == 0x5)
ChunkHeader ‐= 8 * (BYTE)ChunkHeader‐>SegmentOffset;

  LFH上的普通Chunk将具有一个类似于以下内容的头部:

HeapBin chunk

Diagram 14. HeapBin chunk

注意:NextOffset实际上不是一个独立的字段,它只是数据域的前两个字节。

  如果你可以布置一个可溢出Chunk在待释放Chunk的正前方,那么UnusedBytes就可以被赋值成0x5,SegmentOffset可以覆写成你所选择的1字节值,这意味着Header位置可以向前0x0 8到0xFF 8字节。

Overwritten HeapBin chunk

Diagram 15. Overwritten HeapBin chunk

  ChunkHeader基于被覆写的SegmentOffset而重新定位,但其_HEAP_ENTRY必须得合法,因为RtlpLowFragHeapFree()必须得把它释放掉。一眼望去好像没什么卵用,让我们看看下面的代码实例。

  一个高度简单,人为设计的示例(忽略作用域)。它从源获取输入并尝试创建对象和分配内存以供将来使用。由于对要读取的内存量的计算错误,会出现一个缓冲区溢出。我将向您展示这种溢出是如何导致覆盖应用程序数据。

Listing 49. C++ contrived example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Paser
{
public:
Parser();
virtual void DoThings();
~Parser();
private:
int Items;
int Values;
int Stuff;
int Things;
};

void *buffer, *output;
int action, copy_size = 0x40;
Parser *p;

while(1)
{
action = ReadInt();
if(action == 0x1)
p = new Parser();
else if(action == 0x2)
buffer = malloc(sizeof(Parser));
else if(action == 0xFF)
break;

action = ReadInt();
if(action == 0x3)
p.DoThings();
else if(action == 0x4)
ReadBytes(buffer, copy_size);
else if(action == 0x5)
free(buffer);
else if(action == 0x6)
delete p;
else if(action == 0x7)
ReadBytes(buffer, 0x10);
}

  第一件事就是为sizeof(Parser)激活LFH,然后设置内存以便于Parser对象被布置在可以溢出的对象的后面。还需要进行另一次分配,这样溢出的Chunk头部就不会在分配后在RtlpLowFragHeapAllocFromContext()中被重新赋值。这将允许使用用户可控值覆盖相邻的Chunk头。

Chunk setup

Diagram 16. Chunk setup

  在覆盖了Alloc2的头部之后,它会调整Chunk头指向parser对象,我们此后释放Alloc2,这就会使得parser对象所在的内存位置变成了下一个可用Chunk。

Chunk overwrite and free

Diagram 17. Chunk overwrite and free

  现在我们再次分配时,就会得到parser对象的地址,该地址在覆盖vtable之后将在我们的控制之下。因此,通过这第三次的分配加上对p.DoThings()的调用就可以为所欲为。

Step-by-Step(步步为营)
  1. 为sizeof(Parser)激活LFH
  2. 分配一个Parser对象
  3. 分配第一个内存Chunk,它可以溢出(Alloc1)
  4. 分配第二个内存Chunk,用于被溢出覆写头部(Alloc2)
  5. 溢出Alloc1,覆盖Alloc2的头部,UnusedBytes改为0x5,SegOffset改为指向parser对象所需要的blocks的数量
  6. 释放Alloc2
  7. 分配第三个内存Chunk,写入你期望的数据。于是,我们可以覆写parser对象的虚表指针(Alloc3)
  8. 触发对parser对象虚函数的调用(这里以p.DoThings()为例)
Prerequisites(前提条件)
  • 可以控制特定大小的分配
  • 有能力为特定大小的Bucket激活LFH
  • 在被溢出的Chunk前放置一个合法的Chunk
  • 至少溢出8字节,从而可以改变相邻内存Chunk的头
  • 可以释放被覆写的Chunk的能力
Methodology(方法论)
  1. 激活LFH
  2. 标准化堆
  3. Alloc1
  4. Alloc2
  5. Alloc3
  6. 覆写Alloc3(至少8字节)
  7. 释放Alloc3(调整头部指向Alloc1)
  8. Alloc4(实际上指向Alloc1)
  9. 写入数据(污染Alloc1的数据)
  10. 使用Alloc1

FreeEntryOffset Overwrite(覆写FreeEntryOffset)

  本节以一个新技术开始,该技术是我在撰写本论文时调研所得。在前一节中,我们讨论了如何通过跟踪当前Offset和下一个可用块的Offset来管理UserBlock中的块。当前的Offset保存在_INTERLOCK_SEQ结构体中。这样,LFH就知道从何处获取下一个空闲Chunk。

  这样做的主要问题在于下一个FreeEntryOffset存储于Chunk数据域的前两个字节。这意味着分配器必须算出一个值,该值与被管理的Chunk的大小相关,随后被存储到数据域用于下一次迭代。既然LFH中每个Chunk相互之间都是挨着的,并且Chunk头部在分配时不会做校验,那么FreeEntryOffset就可以被覆写成一个新的Offset,这使得后续的分配会指向半任意(semi-arbitrary)位置。

Listing 50. Try/Catch for LFH allocation

1
2
3
4
5
6
7
8
9
10
11
12
try {
//the next offset is stored in the 1st 2‐bytes of userdata
//下一个偏移量存储在userdata的前2个字节中
short NextOffset = UserBlocks + BlockOffset + sizeof(_HEAP_ENTRY);

_INTERLOCK_SEQ AggrExchg_New;
AggrExchg_New.Offset = NextOffset;
}
catch
{
return 0;
}

  有了此知识,通过至少0x9(0xA更好)字节的覆写,我们可以影响到NextOffset的值,从而影响下一次分配。让我们看一个示例。

Listing 51. FreeEntryOffset ovewrite example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class Dollar
{
public:
Dollar();
virtual void INeedDollar();
private:
int Job = 0;
int Dollars = 0;
unsigned int Booze = 0xFFFFFFFF;
};

int first_len, second_len, total_len;
char *dollar1, *dollar2, *dollar3, *data1, *data2;
char *statement = "I need dollar";

first_len = ReadInt();
second_len = ReadInt();

data = alloc(first_len);
ReadBytes(data, first_len);
//int wrap
total_len = first_len + second_len;

//we can write more data than this allocation can hold
//我们可以写入比此分配的Chunk所能容纳的更多的数据
dollar1 = alloc(total_len);
memcpy(dollar1, data, first_len);

//this will store the tainted FreeEntryOffset in the _INTERLOCK_SEQ
//这将在_INTERLOCK_SEQ结构中存储受污染的FreeEntryOffset
dollar2 = alloc(total_len);
memcpy(dollar2, statement, strlen(statement));

//although not on the same UserBlock, it will be allocated on an adjacent page in memory
//虽然不在同一个UserBlock上,但它将分配到内存中相邻的页面上
Dollar dollars[0x20];
for(i = 0; i < 0x20; i++)
{
dollars[i] = new Dollar();
}

//this will give us 0xFFFF * 0x8 bytes of forward range when making an allocation,
//more than enough room to overwrite any of the Dollar objects
//进行分配时,这将为我们提供0xFFFF * 0x8字节的前向范围,有足够的空间覆盖任何Dollar对象
dollar3 = alloc(first_len);
memcpy(dollar3, data, first_len);

//one or more of these can be overwritten with data copied into dollar3
//其中一个或多个可以用复制到dollar3中的数据覆盖
for(i = 0; i < 0x20; i++)
{
dollars[i].INeedDollar();
}

  在此简单函数的开始处,有个明显的integer wrap,后面跟了一个潜在的缓冲区溢出。但是和以往的漏洞利用方法不同的是,我们不能通过简单的覆盖一些元数据来达成代码执行。我们需要覆盖在dollar2数据区域中存储的FreeEntryOffset来控制分配dollar3时返回的地址。

  我们假定LFH为Bucket[0x6](0x30字节)已经启用,也假定dollar1是第一个分配给Bucket[0x6]的Chunk(这只是为了简单起见。对于特定大小,它实际上不需要是LFH的第一个分配)。UserBlock状态看起来如下:

Userblock after chunking

Diagram 18. Userblock after chunking

  在dollar1被分配后,随后的溢出会覆盖下一个空闲堆Chunk,FreeEntryOffset被更新为下一个Chunk的偏移(应是0x0008 chunk)。

FreeEntryOffset Overwrite

Diagram 19. FreeEntryOffset Overwrite

  此时,NextOffset已经被我们控制的值所覆盖,但是为了使用此偏移量我们需要进行下一次分配,这一次该值会存储在_INTERLOCK_SEQ 中以供将来使用。在这个例子中,此分配就是为dollar2分配内存时发生的。

Second allocation setting overwritten FreeEntryOffset

Diagram 20. Second allocation, setting overwritten FreeEntryOffset

  现在我们有了一个0x1501的FreeEntryOffset。该值会超出UserBlock所在的分配页,因此相邻的内存页中的Chunk内容会被覆盖。(并不总是这个值。如果在内存页上有完美的数据可以覆写,那么覆写的Offset可以是0x0000-0xFFFF之间的任意值)。

  在此示例中,相邻内存是在构造0x20 Dollar对象数组时创建的(我们假定它们是0x40字节宽)。一旦Bucket[0x8]的LFH被启用(0x40字节),您将拥有一个类似于下图的整体内存布局:

Multiple UserBlocks

Diagram 21. Multiple UserBlocks

  此时就形成了一个相当有利的局面来覆写函数指针或虚表指针。下一个0x30字节的分配会返回内存地址0x5162016,因为下一个空闲条目是由当前的UserBlock[0x5157800]计算出来的,UserBlocks加上当前偏移(0x0E),最后再加上FreeEntryOffset(0x1501) * 8 。

1
2
3
NextChunk = UserBlock + Depth_IntoUserBlock + (FreeEntryOffset * 8)
NextChunk = 0x5157800 + 0x0E + (0x1501 * 8)
NextChunk = 0x5162016

  这意味着一旦我们分配了dollar3并写入0x30字节的数据,我们就可以覆盖Dollar array中的对象。覆盖的偏移甚至可以调整成指向数组内部特定的对象。尽管该例子非常简单,攻击了应用程序在堆上的数据,但它仍然可以用在多种n字节覆盖的方法中。

Cross page overwrite

Diagram 22. Cross page overwrite

Step-by-Step(步步为营)
  1. 激活LFH
  2. 为你启用LFH的Bucket大小分配一个Chunk(dollar1)
  3. 溢出dollar1至少0x9字节(0xA更好),覆写直接相邻的空闲Chunk,它会在下一次分配中返回
  4. 为你启用LFH的Bucket大小分配一个Chunk(dollar2),它会将被覆盖的NextOffset存储到_INTERLOCK_SEQ中的FreeEntryOffset
  5. 分配一个对象,它用于被覆盖。该分配需要部署在FreeEntryOffset(0x08*0xFFFF是最大值)可触摸的范围内。本例,Dollar对象位于相邻的内存页中。
  6. 为你启用LFH的Bucket大小分配一个Chunk(dollar3),这将返回基于覆盖的FreeEntryOffset所选择的地址
  7. 写入n字节,覆盖关键对象
  8. 调用覆盖的函数指针或虚函数
Prerequisites(前提条件)
  • 可以为特定Bucket激活LFH
  • 可以控制特定Bucket的分配
  • 至少可以覆盖0x9字节,最好0xA字节
  • 可以覆写相邻的空闲Chunk
  • 要覆盖的对象应在最大可触范围(0xFFFF*0x8)内存之内
  • 可以触发使用被覆盖对象的方法
Methodology(方法论)
  1. 激活LFH
  2. 标准化堆
  3. Alloc 1
  4. 覆盖相邻Chunk的NextOffset,之后会被存储在_INTERLOCK_SEQ中的FreeEntryOffset
  5. Alloc 2
  6. Alloc 3
  7. 写入数据到Alloc3(这会覆盖感兴趣的对象)
  8. 触发

Observations(观察结果)

  尽管本节展现的资料更应该放到Exploitation这一部分中,我还是认为放在Observations下更好一些。其背后的原因是在尝试使用此技术来导致代码执行时缺乏可靠性。我想做的最后一件事是实际上将一个感兴趣的项目放在Exploitation部分,就像Sinan Erin所说的那样,是草莓布丁。

SubSegment Overwrite(覆写SubSegment)

  我们在Allocation(分配)一节中看到LFH会在分配内存时试图使用SubSegment。如果当前没有可用的Subsegment,他就会为UserBlock 分配空间,然后继续获取SubSegment。

Listing 52. SubSegment acquisition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
HEAP_SUBSEGMENT *SubSeg = HeapLocalSegmentInfo‐>ActiveSubsegment;

if(SubSeg)
{
while(1)
{
.
.
.
//checks to ensure valid subsegment
//检查以确保是有效的Subsegment
_HEAP_USERDATA_HEADER *UserBlocks = SubSeg‐>UserBlocks;

if(!Depth || !UserBlocks || SubSeg‐>LocalInfo != HeapLocalSegmentInfo)
{
break;
}
.
.
.
}
}
.
.
.
_HEAP_USERDATA_HEADER *UserData = RtlpAllocateUserBlock(lfh, UserBlockCacheIndex, BucketByteSize + 8);

DeletedSubSegment = ExInterlockedPopEntrySList(HeapLocalData);
if (DeletedSubSegment)
{
// if there are any deleted subsegments, use them
// 如果存在被删除的Subsegments,就使用它们
NewSubSegment = (_HEAP_SUBSEGMENT *)(DeletedSubSegment ‐ 0x18);
}
else
{
_HEAP_SUBSEGMENT *NewSubSegment = RtlpLowFragHeapAllocateFromZone(LFH, LocalDataIndex);

//return failure use back‐end
//返回失败,使用后端堆管理器
if(!NewSubSegment)
return 0;
}

//this function will setup the _HEAP_SUBEMENT structure
//and chunk out the data in 'UserData' to be of HeapBucket‐>SizeIndex chunks
//此函数将设置_HEAP_SUBEMENT结构并将“UserData”中的数据分块为HeapBucket->SizeIndex大小的Chunks
RtlpSubSegmentInitialize(LFH,
NewSubSegment,
UserBlock,
RtlpBucketBlockSizes[HeapBucket‐>SizeIndex],
UserDataAllocSize,HeapBucket);
.
.
.

  该执行流通常发生在_HEAP_SUBSEGMENT还没有设置的时候(例如:LFH第一次为特定Bucket分配)或者所有的SubSegments都用完了。这种情况下内存布局看起来如下图:

UserBlock residing before SubSegment pointers in memory

Diagram 23. UserBlock residing before SubSegment pointers in memory

  如你所见,如果你把UserBlock Chunk放置在为SubSegments分配的内存之前,那么溢出就可以覆盖_HEAP_SUBSEGMENT结构所用的指针。尽管Richard Johnson论证了_LFH_BLOCK_ZONE的FreePointer可能会受到损坏,以将_HEAP_SUBSEGMENT结构写入半任意位置(Johnson 2006),我却有个不同的想法。你可以覆写SubSegment,包括UserBlock指针。然后继续进行后续分配,此时就可以用用户提供的指针进行n字节覆写。

Overwrite into _HEAP_SUBSEGMENT

Diagram 24. Overwrite into _HEAP_SUBSEGMENT

Example(示例)

  下面的例子展示了LFH Bin如何为特定大小激活,然后后续的分配覆写了_HEAP_SUBSEGMENT,导致后续分配时使用了污染的数据。注意到覆盖大小为0x200,该值不是一个特定的值,只是用来表明所有_LFH_BLOCK_ZONE项都可以被覆盖。

Listing 53. SubSegment overwrite

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//turn on the LFH
//激活LFH
for(i = 0; i < 0x12; i++)
allocb[i] = HeapAlloc(pHeap, 0x0, SIZE);

//first allocation for SIZE in LFH
//在LFH分配第一个大小为SIZE的Chunk
alloc1 = HeapAlloc(pHeap, 0x0, SIZE);

//get closer in virtual memory to make overwrite easier
//更接近虚拟内存,使覆写更容易
for(i = 0; i < 0x27; i++)
alloc2 = HeapAlloc(pHeap, 0x0, SIZE);

//overwrite the UserBlocks pointer for this SubSegment
//覆盖这个SubSegment的UserBlocks指针
alloc3 = HeapAlloc(pHeap, 0x0, SIZE);
memset(alloc3, 0x41, 0x200);

//this allocation will use the tainted UserBlocks
//这个分配将使用受污染的UserBlocks
alloc4 = HeapAlloc(pHeap, 0x0, SIZE);

Issues(问题)

  这种技术的利用主要有两大阻碍。第一个是需要具备一种对堆可以精准操控的手法。如实例中所展现,UserBlock Chunk需要在内存被用来存储SubSegment指针(_LFH_BLOCK_ZONE)之前分配出来。虽然启用LFH Bin很简单,但在现实情况下,想要保证它布置在连续的内存上且在SubSegment指针所用的Chunk之前会更为困难。你只要去看看Internet Explorer就可以了解到这项任务是多么的困难。

  第二个难点是要避免分配过程中保证SubSegment的完整性的检查。这将确保所请求大小的_HEAP_LOCAL_SEGMENT_INFO结构,和当前存储在_HEAP_SUBSEGMENT的那一个匹配。

Listing 54. SubSegment validation

1
2
3
4
5
6
7
8
9
_HEAP_LOCAL_SEGMENT_INFO *HeapLocalSegmentInfo = HeapLocalData‐>SegmentInfo[HeapBucket‐>SizeIndex];

_HEAP_SUBSEGMENT *SubSeg = HeapLocalSegmentInfo‐>ActiveSubsegment;
.
.
if(!Depth || !UserBlocks || SubSeg‐>LocalInfo != HeapLocalSegmentInfo)
{
break;
}

  虽然Depth和UserBlocks条件很容易被欺骗,但确保LocalInfo结构与存储在LFH中的指针是同一个的检查要复杂一些。使用一个已泄露的Chunk地址是一种解决此问题的可行方案,但是应用程序运行的时间越久,想要可靠地预测一个地址就越困难。最简单的方法就是通过HeapBase来获取FrontEndHeap指针的地址。这将为我们提供一个指向_LFH_HEAP结构的指针,此后相应的_HEAP_LOCAL_SEGMENT_INFO条目地址就可以由请求的Chunk大小来推断出来。

  总的来说,该技术提供了一个非常简单的write-n情景,完全依赖于堆元数据以及需要覆写的地址。不幸的是,在撰写本文之际,还是没能挖掘出一种更为可靠的技巧。这并非不可能,而是我屡试屡败最终选择了放弃。

Conclusion(结论)

  Windows内存管理的各个方面自Windows XP以来都有相当的改变。这些变化的绝大多数最早发生在Windows Vista诞生之初,并一直延续到了Windows 7。

  使用的数据结构更为复杂,对多线程发起的频繁的内存请求有了更好的支持,但仍然与过去的堆实现机制有着一定的相似之处。

  这些新的数据结构使用的风格和之前不同。专用FreeLists这一设计已经被新的更为鲁棒的设计取缔。这些新的技术提供了后端堆管理器去启用前端堆管理器的方式,当前的前端堆管理器只支持LFH,Lookaside链表已经过时了。现在,一种称为UserBlock的新结构在满足某些阈值后将HeapBucket的所有Chunks保存在一个连续的大内存块中。这为频繁的分配和释放提供了更为有效的内存访问方式。

  尽管后来增加了多种安全机制,比如头部编码,反位图翻转(anti-bitmap flipping)以及安全链接(safe linking),依然有着新的精准操控堆的可靠手法诞生。现在相同大小的Chunk可以在内存空间中连续部署,因此覆盖可以更容易预测,并且可以通过简单地控制分配和释放来获取数据播种。

  所有新创建的数据结构和算法都带来了新的复杂性。正如Ben Hawkes之前和我在本文中所展示的那样,这种复杂性可以用来控制执行流。新的偏移量和指针可在简单的溢出情况下使用,以更改堆的状态并提供更好的可靠的执行方式。

  最后,尽管覆写元数据可以改变程序执行流,但对比以往的可用性大打折扣。堆利用变得越来越复杂,并且随着时间的流逝,相比较所想要覆写的数据,对分配和释放操作以及Chunks布局的深层次理解将变得更为重要(更别提破坏DEP和ASLR了)。可能看起来讨论前端和后端管理器到如此深的层次不是很有必要,但是想要游刃有余的操控堆,你必须理解它底层的工作机制。

  • Chris Valasek 2010
    • @nudehaberdasher
    • cvalasek@gmail.com

Bibliography(参考文献)

  1. Hawkes, Ben. 2008. Attacking the Vista Heap. Ruxcon 2008
  2. Hawkes, Ben. 2008. Attacking the Vista Heap. Blackhat USA 2008
  3. Immunity Inc. Immunity Debugger heap library source code. Immunity Inc.
  4. Johnson, Richard. 2006. Windows Vista: Exploitation Countermeasures. Toorcon 8
  5. McDonald/Valasek. 2009. Practical Windows XP/2003 Heap Exploitation. Blackhat USA 2009
  6. Marinescu, Adrian. 2006. Windows Vista Heap Management Enhancements. Blackhat USA 2006
  7. Moore, Brett. 2005. Exploiting Freelist[0] on XP Service Pack 2. Security-Assessment.com White Paper
  8. Moore, Brett. 2008. Heaps About Heaps. SyScan 2008
  9. Probert, David B. (PhD). UserMode Heap Manager
  10. Sotirov, Alexander. 2007. Heap Feng Shui in JavaScript. Black Hat Europe 2007
  11. Vreugdenhil, Peter. 2010. Windows7-InternetExplorer8. Pwn2Own 2010
  12. Waisman, Nico. 2010. (A)leatory (P)ersitent (T)hreat
文章目录
  1. Introduction(简介)
    1. Overview(概述)
    2. Prior Works(先前的工作)
    3. Prerequisites(预备知识)
    4. Terminology(术语)
    5. Notes(说明)
  2. Data Structures(数据结构)
    1. _HEAP
    2. _HEAP_LIST_LOOKUP
    3. _LFH_HEAP
    4. _HEAP_LOCAL_DATA
    5. _LFH_BLOCK_ZONE
    6. _HEAP_LOCAL_SEGMENT_INFO
    7. _HEAP_SUBSEGMENT
    8. _HEAP_USERDATA_HEADER
    9. _INTERLOCK_SEQ
    10. _HEAP_ENTRY
    11. Overview(概览)
  3. Architecture(架构)
    1. FreeLists(WinXP&Win7)
      1. Windows XP
      2. Windows 7
  4. Algorithms(算法)
    1. Allocation(分配)
    2. Back-end Allocation(后端分配器)
      1. RtlpAllocateHeap
      2. Overview(概览)
    3. Front-end Allocation(前端分配器)
      1. RtlpLowFragHeapAllocFromContext
      2. Overview(概览)
      3. Example(示例)
    4. Freeing(释放)
    5. Back-end Freeing(后端释放器)
      1. RtlpFreeHeap
      2. Overview(概览)
    6. Front-end Freeing(前端释放器)
      1. RtlpLowFragHeapFree
      2. Overview(概览)
      3. Example(示例)
  5. Security Mechanisms(安全机制)
    1. Heap Randomization(堆随机化)
      1. Comments(注解)
    2. Header Encoding/Decoding(堆头编码/解码)
      1. Comments(注解)
    3. Death of bitmap flipping(位图翻转的死亡)
    4. Safe Linking(安全链入)
      1. Comments(注解)
  6. Tactics(利用策略)
    1. Heap Determinism(堆确定性)
      1. Activating the LFH(激活LFH)
      2. Defragmentation(碎片整理)
      3. Adjacent Data(相邻的数据)
      4. Seeding Data(播种数据)
    2. Exploitation(漏洞利用)
      1. Ben Hawkes #1
        1. Step-by-Step(步步为营)
        2. Prerequisites(前提条件)
        3. Methodology(方法论)
      2. FreeEntryOffset Overwrite(覆写FreeEntryOffset)
        1. Step-by-Step(步步为营)
        2. Prerequisites(前提条件)
        3. Methodology(方法论)
    3. Observations(观察结果)
      1. SubSegment Overwrite(覆写SubSegment)
      2. Example(示例)
      3. Issues(问题)
  7. Conclusion(结论)
  8. Bibliography(参考文献)